// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Steel Mill gameplay scene

#import "PuzzleCommon.h"
#import "PuzzleScene.h"

// TODO: Fix block being behind mascot sprite during the throw (simple z-order issue)
// TODO: Fix trash stack hanging in the air (maybe when Steelbot throws a block at the top as it arrives?)
// TODO: Fix delay before sound effect when pressing a main menu button

// OPTIONAL fixes:
// TODO: Adjust robot LED one dot down
// TODO: Add more comics bubble sizes and optional text
// TODO: Make tips rotate
// TODO: "New personal record! xxx lines" message as soon as you go past the previous max (and a previous max exists and is > 50 lines)
// TODO: Offer option to take screenshot of stats page
// TODO: Check for iPod music activated during multitasking breaks
// TODO: New "landed-on-melting" category that's excluded from matches, instead of hanging?
// TODO: Figure out a way to make use of trashicon cancelling
// TODO: Make it possible to hold onto a preview through falling trash
// TODO: Add winning pose mascot sprite

// TODO: (for post-release) Predesigned puzzle challenge mode
// TODO: (for post-release) Ingame tutorial
// TODO: (for post-release) "Won challenge mode, grade AAA/AA/A/B/C" message
// TODO: (for post-release) Change physics to stop considering blocks held on melting ones as "landed", and adjust chaining code accordingly.  OR: switch to short melt-time and "pass thru afterglow" method of measuring chains

bool isTouchInSprite(UITouch* touch, CCSprite* sprite) {
	CGSize butsize = [sprite contentSize];
	CGRect butrect = CGRectMake(0,0,butsize.width,butsize.height);
	return CGRectContainsPoint(butrect, [sprite convertTouchToNodeSpace:touch]);
}

CGPoint robotLEDPos(int pose, bool mirrored) {
	int x, y;
	x = 140;
	switch(pose) {
		case 0:
			y = 100;
			break;
		case 1:
			y = 60;
			break;
		case 2:
			x -= 3;
			y = 137;
			break;
		case 3:
			x -= 5;
			y = 157;
			break;
		default:
			x = y = 0;
			break;
	}
	if(mirrored) { x = 225 - x; }

	return ccp(x, y);
}

int robotLEDAngleDegrees(int pose, bool mirrored) {
	int ret = 0;
	if(pose == 2) ret = -10; else if(pose == 3) ret = -13;
	if(mirrored) ret = -ret;
	return ret;
}


@implementation SetTextureAction
+(id) actionWithTexture: (CCTexture2D*) tex {
	return [[[self alloc] initWithTexture:tex] autorelease];
}
-(id) initWithTexture: (CCTexture2D*) tex {
	if( (self = [super init])) {
		texture = tex;
	}

	return self;
}

-(void) startWithTarget:(id)aTarget {
	[super startWithTarget:aTarget];
	[(id<CCTextureProtocol>)aTarget setTexture:texture];
}
@end

@implementation MeltBlockAction
+(id) action {
	return [[[self alloc] init] autorelease];
}
-(id) init {
	if( (self = [super init])) {
	}

	return self;
}

-(void) startWithTarget:(id)aTarget {
	[super startWithTarget:aTarget];
	[(Block*)aTarget meltAnimation];
}
@end

@implementation KillNodeAction
+(id) action {
	return [[[self alloc] init] autorelease];
}
-(id) init {
	if( (self = [super init])) {
	}

	return self;
}

-(void) startWithTarget:(id)aTarget {
	[super startWithTarget:aTarget];

	[(CCNode*)aTarget removeFromParentAndCleanup:true];
}
@end

@implementation SpeedClippedNode
- (id) init {
	if( (self = [super init])) {
	}

	return self;
}

- (void) visit {
	ccDeviceOrientation o = scene->currentOrientation_;
	bool upsidedown;
	upsidedown = (o == kCCDeviceOrientationPortraitUpsideDown);

	int leftclip = 368;
	int rightclip = 716;
	int width = rightclip - leftclip;

	if(upsidedown) {
		leftclip = 768 - rightclip;
	}

	glScissor(leftclip, 0, width, 1024);

	glEnable(GL_SCISSOR_TEST);
	[super visit];
	glDisable(GL_SCISSOR_TEST);
}
@end

@implementation TopClippedSprite
- (id) initInGrid:(GridConfig*)grid {
	if( (self = [super init])) {
		g_ = grid;
		clipped_ = true;
	}

	return self;
}
- (id) initInGrid:(GridConfig*)grid file:(NSString*)file {
	if( (self = [super initWithFile:file])) {
		g_ = grid;
		clipped_ = true;
	}
	
	return self;
}

- (void) visit {
	// Don't draw blocks when paused to prevent cheating
	if(scene->ispaused_) {
		return;
	}

	if(!clipped_) {
		[super visit];
		return;
	}

	ccDeviceOrientation o = scene->currentOrientation_;
	bool portrait, upsidedown;
	portrait = (o < kCCDeviceOrientationLandscapeLeft);
	upsidedown = (o == kCCDeviceOrientationLandscapeRight
			   || o == kCCDeviceOrientationPortraitUpsideDown);
	int fudge = 3;
	if(g_->upsidedown) { upsidedown = !upsidedown; fudge = -1; }

	// Clip blocks vertically to avoid them spilling on the background on the top and bottom
	int botclip = PX_BOTTOM;
	int topclip = GRID_YSIZE*BLOCK_YSIZE+fudge;
	if(portrait) {
		if(upsidedown) {
			botclip = 1024 - topclip - botclip;
			topclip = 1024 - PX_BOTTOM;
		}
		glScissor(0, botclip, 768, topclip);
	} else {
		// landscape
		if(upsidedown) {
			botclip = 768 - topclip - botclip;
			topclip = 768 - PX_BOTTOM;
		}
		glScissor(botclip, 0, topclip, 1024);
	}
	glEnable(GL_SCISSOR_TEST);
	[super visit];
	glDisable(GL_SCISSOR_TEST);
}
@end

// Convert from BlockColor to screen color.
typedef enum DrawColorType_ {
	BG_BRIGHT,
	BG_DIM,
	PLACE_CURSOR,
	MELT_PARTICLE,

	NUM_DRAWCOLOR
} DrawColorType;

ccColor3B htmlColor(const char* s) {
	assert(strlen(s) == 6);
	int r, g, b;
	char buf[3];
	buf[2] = '\0';
	buf[0] = s[0];
	buf[1] = s[1];
	r = strtol(buf, NULL, 16);
	buf[0] = s[2];
	buf[1] = s[3];
	g = strtol(buf, NULL, 16);
	buf[0] = s[4];
	buf[1] = s[5];
	b = strtol(buf, NULL, 16);
	return ccc3(r, g, b);
}

ccColor3B drawColor(BlockColor col, Mascot mascot, DrawColorType type) {
	ccColor3B bright, dim, place;
	switch(col) {
		case RED:
			switch(mascot) {
				case KEITH:
					bright = ccc3(0x4c, 0x03, 0x19);
					dim =    ccc3(0x40, 0x00, 0x13);
					place =  ccc3(0xFF, 0x00, 0x00);
					break;
				case BEAR:
					bright = ccc3(0x54, 0x18, 0x0c);
					dim =    ccc3(0x47, 0x14, 0x0b);
					place =  ccc3(0xFF, 0x00, 0x00);
					break;
				case ROBOT:
					bright = htmlColor("561A05");
					dim    = htmlColor("491302");
					place =  ccc3(0xFF, 0x00, 0x00);
					break;
			}

			break;
		case GREEN:
			switch(mascot) {
				case KEITH:
					bright = ccc3(0x2a, 0x3f, 0x02);
					dim =    ccc3(0x21, 0x33, 0x00);
					place =  ccc3(0x00, 0xFF, 0x00);
					break;
				case BEAR:
					bright = ccc3(0x53, 0x42, 0x07);
					dim =    ccc3(0x39, 0x2e, 0x04);
					place =  ccc3(0xFF, 0xFF, 0x00);
					break;
				case ROBOT:
					bright = htmlColor("604511");
					dim    = htmlColor("491302");
					place =  ccc3(0xFF, 0xFF, 0x00);
					break;
			}
			break;
		case BLUE:
			switch(mascot) {
				case KEITH:
					bright = ccc3(0x00, 0x24, 0x3f);
					dim =    ccc3(0x00, 0x1c, 0x33);
					place =  ccc3(0x00, 0x00, 0xFF);
					break;
				case BEAR:
					bright = ccc3(0x38, 0x00, 0x3b);
					dim =    ccc3(0x2e, 0x00, 0x32);
					place =  ccc3(0xFF, 0x00, 0xFF);
					break;
				case ROBOT:
					bright = htmlColor("2d2d2d");
					dim    = htmlColor("1e1e1e");
					place =  ccc3(0xAA, 0xAA, 0xAA);
					break;
			}
			break;
		case TRASH:
			place = ccc3(0xFF, 0xFF, 0xFF);
			break;
		default:
			assert(false);
	}

	switch(type) {
		case BG_BRIGHT:
			return bright;
		case BG_DIM:
			return dim;
		default:
			return place;
	}
}

// Assumes the counts are adjusted to start at zero
ccColor3B chainColor(int chaincount) {
	switch(chaincount % 14) {
		case 0: return htmlColor("D04854");
		case 1: return htmlColor("D96900");
		case 2: return htmlColor("F0CB05");
		case 3: return htmlColor("96D41A");
		case 4: return htmlColor("1297D4");
		case 5: return htmlColor("9430A8");
		case 6: return htmlColor("E61675");
		case 7: return htmlColor("FF6B6B");
		case 8: return htmlColor("FFA552");
		case 9: return htmlColor("FFEA75");
		case 10: return htmlColor("CAFF61");
		case 11: return htmlColor("45C5FF");
		case 12: return htmlColor("E149FF");
		case 13: return htmlColor("FF82BB"); 
		default: return ccc3(0xFF, 0xFF, 0xFF);
	}
}

@implementation CCParticleWinRain
-(id) init
{
	return [self initWithTotalParticles:100];
}

-(id) initWithTotalParticles:(int)p
{
	if( (self=[super initWithTotalParticles:p]) ) {
	
		// duration
		duration = kCCParticleDurationInfinity;
		
		self.emitterMode = kCCParticleModeGravity;

		// Gravity Mode: gravity
		self.gravity = ccp(10,-10);
		
		// Gravity Mode: radial
		self.radialAccel = 0;
		self.radialAccelVar = 0;
		
		// Gravity Mode: tagential
		self.tangentialAccel = 0;
		self.tangentialAccelVar = 0;

		// Gravity Mode: speed of particles
		self.speed = 400;
		self.speedVar = 100;

		self.startSpin = 0;
		self.endSpin = 2000;
		
		// angle
		angle = -90;
		angleVar = 5;
		
		// life of particles
		life = 4.5f;
		lifeVar = 0;
		
		// size, in pixels
		startSize = 84.0f;
		startSizeVar = 0;
		endSize = kCCParticleStartSizeEqualToEndSize;

		// emits per second
		emissionRate = 20;
		
		// color of particles
		startColor.r = 1.0f;
		startColor.g = 1.0f;
		startColor.b = 1.0f;
		startColor.a = 1.0f;
		startColorVar.r = 0.0f;
		startColorVar.g = 0.0f;
		startColorVar.b = 0.0f;
		startColorVar.a = 0.0f;
		endColor.r = 1.0f;
		endColor.g = 1.0f;
		endColor.b = 1.0f;
		endColor.a = 0.5f;
		endColorVar.r = 0.0f;
		endColorVar.g = 0.0f;
		endColorVar.b = 0.0f;
		endColorVar.a = 0.0f;
		
		// additive
		self.blendAdditive = NO;

		self.autoRemoveOnFinish = true;
	}
	
	return self;
}
@end

@implementation Block

@dynamic gridpos;
@synthesize col;
@dynamic shape;

@synthesize isbeingthrown;
@synthesize ishanging;
@synthesize isfalling;
@synthesize ismelting;
@synthesize neverlanded;
@synthesize chaincount;
@synthesize chaingroup;
@synthesize melttime;
@synthesize falldelay;

+ (id) blockInGrid:(GridConfig*)grid pos:(CGPoint)gridpt color:(BlockColor)c shape:(BlockShape)s {
	return [[[Block alloc] initInGrid:grid pos:gridpt color:c shape:s] autorelease];
}

- (id) initInGrid:(GridConfig*)grid pos:(CGPoint)gridpt color:(BlockColor)c shape:(BlockShape)s {
	if(c == TRASH) {
		switch(s) {
			case TRASHx0:
				self = [super initInGrid:grid file:@"Assets/Graphics/trashblock0.png"];
				break;
			case TRASHx1:
				self = [super initInGrid:grid file:@"Assets/Graphics/trashblock1.png"];
				break;
			case TRASHx2:
				self = [super initInGrid:grid file:@"Assets/Graphics/trashblock2.png"];
				break;
			case TRASHx3:
				self = [super initInGrid:grid file:@"Assets/Graphics/trashblock3.png"];
				break;
			default:
				assert(false);
				break;
		}
	} else {
		self = [super initInGrid:grid];
		[super setTexture:blocktex[grid->mascot-1][c - RED]];
		CCTexture2D *hearttexture = [[heartanims[grid->mascot-1][c - RED].frames lastObject] texture];

		heart1 = [CCSprite spriteWithTexture:hearttexture];
		heart2 = [CCSprite spriteWithTexture:hearttexture];
		[self addChild: heart1];
		[self addChild: heart2];
		heart1.position = ccp(TEXTURE_XUNIT/2, TEXTURE_YUNIT/2);
		heart2.position = ccp(TEXTURE_XUNIT*2.5, TEXTURE_YUNIT/2);
	}
	if(self == nil) return nil;

	col = c;
	[self setShape:s];
	gridpos = gridpt;
	[self refreshTexture];

	neverlanded = true;
	chaingroup = -1;

	[super setScale:SCALEFACTOR];

	return self;
}

- (void) glow {
#if BLOCKGLOW
	[self runAction:[CCRepeatForever actionWithAction:
							[CCSequence actions: 
						    [CCTintTo actionWithDuration:1.0 red:0xCC green:0xCC blue:0xCC],
						    [CCTintTo actionWithDuration:1.0 red:0xFF green:0xFF blue:0xFF],
							nil]]];
#endif // #if BLOCKGLOW
}

- (void) animate {
	if(col == EMPTY || col == TRASH) return;
	CCAnimation* anim = heartanims[g_->mascot-1][col - RED];

	[heart1 addAnimation:anim];
	[heart2 addAnimation:anim];
	[heart1 runAction:[CCRepeatForever actionWithAction:
							[CCSequence actions: 
						    [CCAnimate actionWithAnimation:anim],
	                     	[CCDelayTime actionWithDuration:0.1], 
							nil]]];
	[heart2 runAction:[CCRepeatForever actionWithAction:
							[CCSequence actions: 
						    [CCAnimate actionWithAnimation:anim],
	                     	[CCDelayTime actionWithDuration:0.1], 
							nil]]];
}

- (void) makeTutorialBlock:(id)sender {
	heart1.opacity = heart2.opacity = self.opacity = 0xA0;
	heart1.color = heart2.color = self.color = drawColor(col, g_->mascot, PLACE_CURSOR);
	[heart1 setTexture:[[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/heart-tutorialoutline.png"]]];
	[heart2 setTexture:[[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/heart-tutorialoutline.png"]]];
	[self setTexture:[[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/block-tutorialoutline.png"]]];

	tutorialhand = [CCSprite spriteWithFile:@"Assets/Graphics/hand-icon.png"];
	tutorialhand.anchorPoint = ccp(0.7, 1.0);
	[tutorialhand runAction:[CCRepeatForever actionWithAction:
							[CCSequence actions: 
							[CCPlace actionWithPosition:heart1.position],
	                     	[CCDelayTime actionWithDuration:0.6], 
							[CCMoveTo actionWithDuration:0.7 position:heart2.position],
	                     	[CCDelayTime actionWithDuration:0.3], 
							nil]]];
	[self addChild:tutorialhand z:5];
}

- (void) dealloc {
	[super cleanup];
	[super removeAllChildrenWithCleanup:false];
	// Hearts, animations and particles held by parent
	[super dealloc];
}

- (CGPoint) gridpos { return gridpos; }

- (void) setGridpos:(CGPoint)pt {
	gridpos = pt;
	[self updateDisplay];
}

- (void) updateDisplay {
	if(isbeingthrown) return;
	CGRect pxrect = [self pxRect];
	CGPoint midpos = ccp(CGRectGetMidX(pxrect), CGRectGetMidY(pxrect));
	super.position = midpos;
}

- (void) refreshTexture {
	int xoffset = 0;
	int yoffset = 0;
	int xsize = [self xSize] * TEXTURE_XUNIT;
	int ysize = [self ySize] * TEXTURE_YUNIT;
	if(shape & UP || shape & DOWN) { int t = xsize; xsize = ysize; ysize = t; }
	
	[super setTextureRect:CGRectMake(xoffset,yoffset,xsize,ysize)];
	if(col != EMPTY && col != TRASH) {
		[super setTexture:blocktex[g_->mascot-1][col - RED]];
		[heart1 setDisplayFrame:[[heartanims[g_->mascot-1][col - RED] frames] objectAtIndex: 4]];
		[heart2 setDisplayFrame:[[heartanims[g_->mascot-1][col - RED] frames] objectAtIndex: 4]];
	}

	if(shape == LEFTRIGHT || shape == UPDOWN) heart2.visible = true; else heart2.visible = false;
}



- (BlockShape) shape { return shape; }

- (void) setShape:(BlockShape)s {
	shape = s;
	int rot;
	switch(s) {
		case LEFT:
		case LEFTRIGHT:
			rot = 0; break;
		case RIGHT:
			rot = 180; 
			super.flipY = true; // flip back texture for the shadow.
			                    // note that the line of symmetry for flipY is the x axis
			break;
		case UP:			
		case UPDOWN:		
			rot = 90; break;
		case DOWN:			
			rot = 270;
			super.flipY = true; // flip back texture to prevent misaligned look
			                    // note that line of symmetry is rotated along with the sprite
			break;
			
		case TRASHx0:		
		case TRASHx1:		
		case TRASHx2:		
		case TRASHx3:		
			rot = 0;
			break;
		default:            assert(false);
	}
	[super setRotation: rot];
	[heart1 setRotation: -rot];
	[heart2 setRotation: -rot];

	[self refreshTexture];
}

- (int) xSize {
	switch (shape) {
		case LEFT:
		case RIGHT:
		case LEFTRIGHT:
			return 3;
		case UP:
		case DOWN:
		case UPDOWN:
			return 1;

		case TRASHx0:
			return 8;
			
		case TRASHx1:
		case TRASHx2:
		case TRASHx3:
			return 4;
		default:            assert(false); return -1;
	}
}

- (int) ySize {
	switch (shape) {
		case LEFT:
		case RIGHT:
		case LEFTRIGHT:
			return 1;
		case UP:
		case DOWN:
		case UPDOWN:
			return 3;
			
		case TRASHx0:
		case TRASHx1:
			return 1;
		case TRASHx2:	
			return 2;
		case TRASHx3:	
			return 3;
		default:			assert(false); return -1;
	}
}


- (int) numHearts {
	switch (shape) {
		case LEFT:
		case RIGHT:
		case UP:
		case DOWN:
			return 1;
		case LEFTRIGHT:
		case UPDOWN:
			return 2;
			
		case TRASHx0:
		case TRASHx1:
		case TRASHx2:
		case TRASHx3:
			return 0;
		default:			assert(false); return -1;
	}
}


- (CGRect) gridRect {
	CGRect rect;
	rect.origin.x = gridpos.x;
	rect.origin.y = gridpos.y;
	rect.size.width = [self xSize];
	rect.size.height = [self ySize];
	return rect;
}

- (CGRect) pxRect {
	CGRect gridrect, pxrect;
	gridrect = [self gridRect];
	pxrect.origin.x = gridrect.origin.x * BLOCK_XSIZE;
	pxrect.origin.y = gridrect.origin.y * BLOCK_YSIZE;
	pxrect.size.width = gridrect.size.width * BLOCK_XSIZE;
	pxrect.size.height = gridrect.size.height * BLOCK_YSIZE;
	return pxrect;
}

- (CGPoint) heartPos {
	CGPoint heartpos = gridpos;
	if(shape & UP) {
		heartpos.y += 2;
	}
	if(shape & RIGHT) {
		heartpos.x += 2;
	}
	return heartpos;
}

// Returns the number of hearts melted
- (int) startMelt:(int)fallerchaincount chaingroup:(int)fallerchaingroup {
	if(ismelting) return 0;
	
	ismelting = true;
	chaincount = fallerchaincount;
	chaingroup = fallerchaingroup;

	[self meltAnimation];

	return [self numHearts];
}

- (void) meltAnimation {
	// Blink
    CCSprite* overlay;
	if(col == TRASH) {
		overlay = [[[CCSprite alloc] init] autorelease];
		[overlay setColor:ccc3(0xFF,0xFF,0xFF)];
		int xsize = TEXTURE_XUNIT * [self xSize];
		int ysize = TEXTURE_YUNIT * [self ySize];
		overlay.position = ccp(xsize / 2, ysize / 2);
		[overlay setTextureRect:CGRectMake(0, 0, xsize, ysize)];

		int trashno = shape >> 8;
		if(trashno == 4) trashno = 3;
		[self setTexture:[[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/trashblock%d-clear.png", trashno]]];
	} else {
		overlay = [CCSprite spriteWithFile:@"Assets/Graphics/block-whiteoverlay.png"];
		overlay.position = ccp(TEXTURE_XUNIT*3/2,TEXTURE_YUNIT/2);
	}
	[overlay setOpacity:0xA0];
    [self addChild: overlay];
	double blinklen = MELTTIME-0.2;
	int numblinks = (10 * MELTTIME/1.7);
	[overlay runAction:[CCSequence actions:[CCBlink actionWithDuration:blinklen blinks:numblinks],
								           [CCHide action],
										   nil]];
	CCAction* fadeaftermelt1 = [CCSequence actions:[CCDelayTime actionWithDuration:blinklen],
								           [CCFadeOut actionWithDuration:0.2],
										   nil];
	CCAction* fadeaftermelt2 = [CCSequence actions:[CCDelayTime actionWithDuration:blinklen],
								           [CCFadeOut actionWithDuration:0.2],
										   nil];
	CCAction* fadeaftermelt3 = [CCSequence actions:[CCDelayTime actionWithDuration:blinklen],
								           [CCFadeOut actionWithDuration:0.2],
										   nil];
	[self runAction:fadeaftermelt1];
	[heart1 runAction:fadeaftermelt2];
	[heart2 runAction:fadeaftermelt3];

	CCAction* fadeemitter[2];
	fadeemitter[0] = [CCSequence actions:[CCDelayTime actionWithDuration:blinklen + 0.3],
								           [CCHide action],
										   nil];
	fadeemitter[1] = [CCSequence actions:[CCDelayTime actionWithDuration:blinklen + 0.3],
								           [CCHide action],
										   nil];
	// Particles
	for(int i = 0; i < 2; i++) {
		if(i == 1 && !heart2.visible) break;
		emitter[i] = [[[CCParticleFlower alloc] initWithTotalParticles:75] autorelease];
		[self addChild: emitter[i]];
		emitter[i].texture = [[CCTextureCache sharedTextureCache] addImage: @"Assets/Graphics/sparkle.png"];
		emitter[i].position = i == 0 ? heart1.position : heart2.position;
		if(col == TRASH) {
			emitter[i].position = overlay.position;
		}
		emitter[i].rotation = heart1.rotation - scene.rotation - (g_->upsidedown ? 180 : 0);
		emitter[i].startSize = 30.0f;
		emitter[i].speed = 160;
		emitter[i].life = 1;
		emitter[i].lifeVar = 0;

		ccColor3B c = drawColor(col, g_->mascot, MELT_PARTICLE);
		ccColor4F startColor = {c.r, c.g, c.b, 1.0f};
		emitter[i].startColor = startColor;
		
		ccColor4F startColorVar = {0.5f, 0.5f, 0.5f, 0.2f};
		emitter[i].startColorVar = startColorVar;
		emitter[i].autoRemoveOnFinish = true;

		[emitter[i] runAction:fadeemitter[i]];
	}
}

- (void) startFalling:(int)meltedchaincount chaingroup:(int)meltedchaingroup delay:(float)delay {
	if(isfalling) return;
	
	isfalling = true;
	falldelay = delay;
	if (delay > 0) { ishanging = true; }
	chaincount = meltedchaincount;
	chaingroup = meltedchaingroup;
}


NSComparisonResult isHigherThan(Block* b1, Block* b2, void* context) {
	if(b1 == nil || b2 == nil) return NSOrderedSame;
	float y1 = b1.gridpos.y;
	float y2 = b2.gridpos.y;
	if(y1 < y2) return NSOrderedAscending;
	if(y1 > y2) return NSOrderedDescending;
	return NSOrderedSame;

}
@end


@implementation TrashIcon

@synthesize block_;

- (id) initWithBlock:(Block*)block chaingroup:(int)chaingroup {
	switch(block.shape) {
		case TRASHx1:
			self = [super initWithFile:@"Assets/Graphics/trashicon-1.png"];
			break;
		case TRASHx2:
			self = [super initWithFile:@"Assets/Graphics/trashicon-2.png"];
			break;
		case TRASHx3:
			self = [super initWithFile:@"Assets/Graphics/trashicon-3.png"];
			break;
		default:
			assert(false);
			break;
	}

	block_ = block;
	block->trashicon = self;
	chaingroup_ = chaingroup;

	return self;
}

@end

@implementation ChaingroupStats
@end


@implementation PuzzleStack

@synthesize won_;
@synthesize robotmaxed_;
@synthesize blocks_;
@synthesize opponent_;
@synthesize g_;
@synthesize winlabel_;
@synthesize mascot_;

-(id) initWithGridConfig:(GridConfig*) grid common:(PuzzleCommon*) common {
	if( (self=[super init] )) {
		g_ = grid;
		common_ = common;
		[common_ retain];

		blocks_ = [[NSMutableArray alloc] init];
		trashqueue_ = [[NSMutableArray alloc] init];
		trashiconqueue_ = [[NSMutableArray alloc] init];

		chaingroupstats_ = [[NSMutableDictionary alloc] init];

		blocksdisplay_ = [CCNode node];
		blocksdisplay_.position = ccp(PX_LEFT, PX_BOTTOM);
		[blocksdisplay_ setContentSize:CGSizeMake(PX_XSIZE, PX_YSIZE)];

		blocksdisplaybase_ = [CCNode node];
		blocksdisplaybase_.position = ccp(PX_LEFT, PX_BOTTOM);
		[blocksdisplaybase_ setContentSize:CGSizeMake(PX_XSIZE, PX_YSIZE)];
		[self addChild:blocksdisplaybase_ z:BLOCKSDISPLAY_Z+1];

		// Maybe decrease memory usage?  TODO: profile.  
		// Brought back to 8888 below.
		[CCTexture2D setDefaultAlphaPixelFormat:kTexture2DPixelFormat_RGBA4444];

		background_ = [[[CCSprite alloc] init] autorelease];
		[background_ setTextureRect:CGRectMake(0, 0, PX_XSIZE, PX_YSIZE)];
		[self addChild:background_ z:BACKGROUND_Z];
		background_.position = ccp(PX_LEFT + (PX_XSIZE / 2), PX_BOTTOM + (PX_YSIZE / 2));
		[background_ setOpacity: 0x0];
		[background_ setColor:ccc3(0,0,0)];

		// Vertical background slices
		CCSprite* slice;
		for(int i = 0; i < 8; i++) {
			slice = [[[CCSprite alloc] init] autorelease];
			[slice setTextureRect:CGRectMake(0, 0, BLOCK_XSIZE, PX_YSIZE)];
			[self addChild:slice z:BACKGROUNDSLICE_Z];
			[slice setColor:ccc3(0,0,0)];
			slice.position = ccp(PX_LEFT + (BLOCK_XSIZE * i), PX_BOTTOM);
			slice.anchorPoint = ccp(0, 0);
			slices_[i] = slice;
		}

		scrolldarkener_ = [[[CCSprite alloc] init] autorelease];
		[scrolldarkener_ setTextureRect:CGRectMake(0, 0, PX_XSIZE, PX_YSIZE)];
		[scrolldarkener_ setColor: ccc3(0,0,0)];
		[scrolldarkener_ setOpacity: 0xA0];
		scrolldarkener_.position = ccp(PX_XSIZE / 2, -(PX_YSIZE / 2));
		[blocksdisplay_ addChild:scrolldarkener_ z:SCROLLDARKENER_Z];

#if THREEARROWS
		int xdiff = PX_XSIZE / 2 - 50;
		CGPoint arrowpos = ccp(PX_XSIZE / 2 - xdiff, 30);
		for(int i = 0; i < 3; i++) {
			scrollarrows_[i] = [CCSprite spriteWithFile:@"Assets/Graphics/arrow-stackraise.png"];
			scrollarrows_[i].position = arrowpos;
			scrollarrows_[i].visible = false;
			[blocksdisplay_ addChild:scrollarrows_[i] z:SCROLLDARKENER_Z];
			arrowpos.x += xdiff;
		}
#else
		scrollarrows_[0] = [CCSprite spriteWithFile:@"Assets/Graphics/raise-stack.png"];
		scrollarrows_[0].visible = false;
		scrollarrows_[0].anchorPoint = ccp(0, 0);
		scrollarrows_[0].position = ccp(0, 0);
		scrollarrows_[0].opacity = 0x80;
		[blocksdisplay_ addChild:scrollarrows_[0] z:SCROLLDARKENER_Z];
#endif // #if THREEARROWS

		foreground_ = [[[CCSprite alloc] init] autorelease];
		[foreground_ setTextureRect:CGRectMake(0, 0, PX_XSIZE, PX_YSIZE + 1)];
		[foreground_ setColor: ccc3(0,0,0)];
		[foreground_ setOpacity: 0x70];
		foreground_.position = ccp(PX_LEFT + PX_XSIZE / 2, PX_YSIZE / 2);
		[self addChild:foreground_ z:FOREGROUND_Z];

		dangerglow_ = [[[CCSprite alloc] init] autorelease];
		[dangerglow_ setTextureRect:CGRectMake(0, 0, g_->playerareasize_x, g_->playerareasize_y)];
		[dangerglow_ setColor: ccc3(0xFF,0,0)];
		[dangerglow_ setOpacity: 0];
		dangerglow_.position = ccp(g_->playerareastart_x, 0);
		dangerglow_.anchorPoint = ccp(0,0);
		[self addChild:dangerglow_ z:DANGERGLOW_Z];

		[CCTexture2D setDefaultAlphaPixelFormat:kTexture2DPixelFormat_RGBA8888];

		if(scene->currentOrientation_ == kCCDeviceOrientationPortrait ||
			scene->currentOrientation_ == kCCDeviceOrientationPortraitUpsideDown) {
			[self setContentSize:CGSizeMake(768,1024)];
		} else {
			[self setContentSize:CGSizeMake(1024,768)];
		}
		if(g_->upsidedown) {
			self.rotation = 180;
		}
		CGPoint pos = self.position;
		pos.x = g_->offsetx;
		self.position = pos;

		trashicondisplay_ = [CCNode node];
		trashicondisplay_.position = ccp(PX_LEFT + (g_->mirrored ? 0 : PX_XSIZE) + ICONXOFFSET, PX_TOP);
		[self addChild:blocksdisplay_ z:BLOCKSDISPLAY_Z];
		[self addChild:trashicondisplay_ z:TRASHICONDISPLAY_Z];

		// Init mascot with blank texture to fill in after player selection.
		mascot_ = [CCSprite spriteWithTexture:emptytex];
		mascot_.scale = MASCOTSCALE;
		mascot_.position = MASCOTPOS;
		mascot_.anchorPoint = ccp(0.5, 0);
		if(g_->mirrored) { mascot_.flipX = true; }
		[self addChild:mascot_ z:MASCOT_Z];
		[mascot_ setTextureRect: CGRectMake(0, 0, 225, 400)];

		fingers_ = [CCSprite spriteWithTexture:emptytex];
		fingers_.scale = MASCOTSCALE;
		fingers_.position = MASCOTPOS;
		fingers_.anchorPoint = ccp(0.5, 0);
		[self addChild:fingers_ z:MASCOT_Z+1];

		shadow_ = [CCSprite spriteWithFile:@"Assets/Graphics/char-shadow.png"];
		if(scene.layout_ == SINGLEPLAYER) {
			shadow_.position = ccp(80, 10);
		} else {
			if(g_->mirrored) {
				shadow_.position = ccp(90, 10);
			} else {
				shadow_.position = ccp((225-90), 10);
			}
		}
		shadow_.anchorPoint = ccp(0.5, 0.5);
		[mascot_ addChild:shadow_ z:-1];

		for(int i = 0; i <= 9; i++) {
			robotLEDtex_[i] = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/nums/%d.png", ROBOT, i]];
			maxLEDtex_[i] = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/steelbot-menu-nums/%d.png", i]];
		}
		robotLEDbox_ = [CCNode node];
		robotlogo_ = [CCSprite spriteWithFile:@"Assets/Graphics/char3-v2/nums/logo.png"];
		robotlogo_.position = ccp(0, 50);
		[robotLEDbox_ addChild:robotlogo_];

		int maxspeed = [[NSUserDefaults standardUserDefaults] integerForKey:D_ROBOTMAXSPEED];
		if(g_->ailevel == maxspeed - 1 || ROBOTMAXED) {
			robotmaxed_ = true;
		}
		if(!robotmaxed_) {
			[self setRobotLED:g_->ailevel+1];
		} else {
			robotLED_[0] = [CCSprite spriteWithFile:@"Assets/Graphics/char3-v2/nums/max.png"];
			robotLED_[0].position = ccp(0, -2);
			[robotLEDbox_ addChild:robotLED_[0]];
		}
			
		[mascot_ addChild:robotLEDbox_ z:10];

		speechbubble_ = [CCSprite spriteWithFile:@"Assets/Graphics/hint-bubble.png"];
		speechbubble_.position = ccp(225, 280);
		speechbubble_.scale = 0.7;
		speechbubble_.visible = false;
		if(!g_->mirrored) {
			speechbubble_.flipX = true;
			speechbubble_.position = ccp(0, 280);
		}
		[fingers_ addChild:speechbubble_];

		raisefingersbox_ = [CCNode node];
		raisefingers_[0] = [CCSprite spriteWithFile:@"Assets/Graphics/hand-icon.png"];
		raisefingers_[1] = [CCSprite spriteWithFile:@"Assets/Graphics/hand-icon.png"];
		raisefingers_[0].position = ccp(-50, 0);
		raisefingers_[1].position = ccp(50, 0);
		raisefingers_[0].visible = false;
		raisefingers_[1].visible = false;
		[raisefingersbox_ addChild:raisefingers_[0]];
		[raisefingersbox_ addChild:raisefingers_[1]];
		[raisefingersbox_ runAction:[CCRepeatForever actionWithAction:
						[CCSequence actions: 
						[CCPlace actionWithPosition:ccp(PX_XSIZE/2, 100)],
						[CCMoveTo actionWithDuration:1.5 position:ccp(PX_XSIZE/2, 300)],
						[CCDelayTime actionWithDuration:0.3], 
						nil]]];
		[blocksdisplaybase_ addChild:raisefingersbox_ z:CURSOR_Z];

		currentblock_ = [Block blockInGrid:g_ pos:ccp(-1,-1)
											color:[common_ getBlockColorToPlace:blocksplaced_]
											shape:[common_ getBlockDoublenessToPlace:blocksplaced_]
												? LEFT : LEFTRIGHT];
		nextblock_ = [Block blockInGrid:g_ pos:ccp(-1,-1) 
											color:[common_ getBlockColorToPlace:blocksplaced_+1]
											shape:[common_ getBlockDoublenessToPlace:blocksplaced_+1]
												? LEFT : LEFTRIGHT];
		[currentblock_ setScale:1];
		[nextblock_ setScale:1];
		[mascot_ addChild:currentblock_ z:3];
		[mascot_ addChild:nextblock_ z:2];
		currentblock_.anchorPoint = ccp(0.5,0.5);
		nextblock_.anchorPoint = ccp(0.5,0.5);
		currentblock_.visible = nextblock_.visible = false;

		cursor_ = [CCSprite spriteWithFile:@"Assets/Graphics/cursor.png"];
		cursor_.visible = false;
		[blocksdisplay_ addChild:cursor_ z:CURSOR_Z];
		previewblock_ = [Block blockInGrid:g_ pos:ccp(0,0) color:RED shape:LEFT];
		previewblock_.visible = false;
		[blocksdisplay_	addChild:previewblock_ z:CURSOR_Z];

		for(int i = 0; i < GRID_YSIZE; i++) {
			[self addBlocksAtRaiseLevel:-i];
		}

		if(opponent_ == nil) {
			vacationsquare_ = [CCSprite spriteWithFile:@"Assets/Graphics/vacation-time-bg.png"];
			vacationsquare_.position = DANGERSIGNPOS;
			[self addChild:vacationsquare_ z:SIDE_ELEMENT_Z-1];

			vacationlabel_ = [CCLabel labelWithString:@"seconds" fontName:@"BPreplayBold" fontSize:30];
			CGPoint pos = DANGERSIGNPOS;
			pos.y -= 75;
			vacationlabel_.position = pos;
			[self addChild:vacationlabel_ z:SIDE_ELEMENT_Z];

			vacationtimelabel_ = [CCLabel labelWithString:@" " fontName:@"BPreplayBold" fontSize:60];
			vacationtimelabel_.position = ccp(DANGERSIGNPOS.x, DANGERSIGNPOS.y - 30);
			[self addChild:vacationtimelabel_ z:SIDE_ELEMENT_Z];
			[self setVacationString];
		}

#if DEPTHLABEL
		depthlabel_ = [CCLabel labelWithString:@"0  lines" fontName:@"Franchise" fontSize:30];
		depthlabel_.position = DEPTHPOS;
		[self addChild:depthlabel_ z:SIDE_ELEMENT_Z];
#endif // #if DEPTHLABEL
		[self setDepthString];

		layoutname_ = nil;
		switch(scene.layout_) {
			case SINGLEPLAYER:
				layoutname_ = "orientation-sidebyside";
				break;
			case SINGLEVSAI:
			case TWOPLAYERS:
				if(g_->playerno == 0) 
					layoutname_ = "orientation-sidebyside";
				break;
			case TWOPLAYERSREVERSED:
				layoutname_ = "orientation-oppsides";
				break;
		}
		if(layoutname_) {
			pausebutton_ = [CCSprite spriteWithFile: [NSString stringWithFormat:@"Assets/Graphics/%s/pause-btn.png", layoutname_]];
			pausebutton_.position = PAUSEPOS;
			pausebutton_.anchorPoint = ccp(0,0);
			[self addChild:pausebutton_ z:SIDE_ELEMENT_Z];
			helpbutton_ = [CCSprite spriteWithFile: [NSString stringWithFormat:@"Assets/Graphics/%s/help-btn.png", layoutname_]];
			helpbutton_.position = HELPPOS;
			helpbutton_.anchorPoint = ccp(0,0);
			[self addChild:helpbutton_ z:SIDE_ELEMENT_Z];
			menubutton_ = [CCSprite spriteWithFile: [NSString stringWithFormat:@"Assets/Graphics/%s/menu-btn.png", layoutname_]];
			menubutton_.position = MENUPOS;
			menubutton_.anchorPoint = ccp(0,0);
			[self addChild:menubutton_ z:SIDE_ELEMENT_Z];
			timerbg_ = [CCSprite spriteWithFile: [NSString stringWithFormat:@"Assets/Graphics/%s/timer-bg.png", layoutname_]];
			timerbg_.position = TIMERPOS;
			timerbg_.anchorPoint = ccp(0,0);
			[self addChild:timerbg_ z:SIDE_ELEMENT_Z];

			[self updateTimer];
		}

		if(scene.layout_ != SINGLEPLAYER) {
			trashiconbg_ = [CCSprite spriteWithFile:@"Assets/Graphics/trash-area.png"];
			trashiconbg_.anchorPoint = ccp(0,0);
			trashiconbg_.position = ccp(PX_LEFT, PX_TOP);
			[self addChild:trashiconbg_ z:SIDE_BACKGROUND_Z];
			if(g_->playerno == 0) {
				centerbg_ = [CCSprite spriteWithFile:@"Assets/Graphics/center-area-left.png"];
				centerbg_.anchorPoint = ccp(1.0, 0);
				trashiconbg_.flipX = true;
			} else {
				centerbg_ = [CCSprite spriteWithFile:@"Assets/Graphics/center-area-right.png"];
				centerbg_.anchorPoint = ccp(0, 0);
				centerbgpatch_ = [CCSprite spriteWithFile:@"Assets/Graphics/center-area-right.png"];
				centerbgpatch_.anchorPoint = ccp(0.5, 0);
				if(scene.layout_ == TWOPLAYERSREVERSED) {
					centerbgpatch_.position = ccp(0, 0);
				} else {
					centerbgpatch_.position = ccp(512, 0);
				}
				[self addChild:centerbgpatch_ z:SIDE_BACKGROUND_Z-1];
			}
			if(scene.layout_ == TWOPLAYERSREVERSED) {
				centerbg_.position = ccp(0, 0);
				centerbg_.anchorPoint = ccp(0, 0);
				if(g_->playerno == 1) centerbg_.flipY = true; else centerbg_.flipX = true;
			} else {
				centerbg_.position = ccp(512, 0);
			}
			[self addChild:centerbg_ z:SIDE_BACKGROUND_Z];
		} else {  // SINGLEPLAYER
			trashiconbg_ = [CCSprite spriteWithFile:@"Assets/Graphics/trash-area-1p.png"];
			trashiconbg_.anchorPoint = ccp(0,0);
			trashiconbg_.position = ccp(PX_LEFT, PX_TOP);
			[self addChild:trashiconbg_ z:SIDE_BACKGROUND_Z];
			centerbg_ = [CCSprite spriteWithFile:@"Assets/Graphics/left-area-1p.png"];
			centerbg_.anchorPoint = ccp(0, 0);
			[self addChild:centerbg_ z:SIDE_BACKGROUND_Z];
		}

		heartsrecord_ = [[NSUserDefaults standardUserDefaults] integerForKey:D_HEARTSCLEARED];

		linesreached_ = [CCNode node];
		linesreachedlabel_ = [CCLabel labelWithString:@" " fontName:@"Franchise" fontSize:70];
		sloweddownlabel_ = [CCLabel labelWithString:@"Stack speed temporarily slowed." fontName:@"Franchise" fontSize:40];
		linesreachedlabel_.position = ccp(0, 30);
		sloweddownlabel_.position = ccp(0, -30);
		sloweddownlabel_.color = htmlColor("BBBBBB");
		linesreached_.visible = false;

		newrecordlabel_ = [CCLabel labelWithString:@"New record!"  fontName:@"Franchise" fontSize:60];
		newrecordlabel_.position = ccp(0, -100);
		newrecordlabel_.color = htmlColor("FFFFFF");

		[linesreached_ addChild:linesreachedlabel_];
		[linesreached_ addChild:sloweddownlabel_];
		[linesreached_ addChild:newrecordlabel_];
		[self addChild:linesreached_ z:WINLOSE_Z];

		ingamestate_ = MASCOTSELECT;

		[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];

		if(g_->is_ai) {
			[self setMascot:ROBOT byPlayer:false];
			[self setDifficulty:MEDIUM byPlayer:false];
			g_->is_ready = true;

			stealjob_ = [CCLabel labelWithString:@"S.T.E.E.L.bot wants to \"steel\" your job!  Show him how real men work!!"  
					   	   dimensions:CGSizeMake(PX_XSIZE - 10, PX_YSIZE)
                           alignment:UITextAlignmentCenter
						   fontName:@"Franchise"
						   fontSize:60];
			stealjob_.position = ccp(PX_LEFT + PX_XSIZE/2, PX_YSIZE * 0.4);
			stealjob_.visible = false;
			[self addChild:stealjob_ z:WINLOSE_Z];

			int maxspeed = [[NSUserDefaults standardUserDefaults] integerForKey:D_ROBOTMAXSPEED];

			maxbotlevel_ = [CCSprite spriteWithFile:@"Assets/Graphics/steelbot-start-menu.png"];
			maxbotlevel_.position = ccp(PX_LEFT, PX_BOTTOM);
			maxbotlevel_.anchorPoint = ccp(0, 0);
			[self addChild:maxbotlevel_ z:WINLOSE_Z];
			maxnums_[0] = [CCSprite spriteWithTexture:maxLEDtex_[MIN(maxspeed / 10, 9)]];
			maxnums_[1] = [CCSprite spriteWithTexture:maxLEDtex_[maxspeed % 10]];
			maxnums_[0].position = ccp(240, PX_TOP - 272);
			maxnums_[1].position = ccp(240 + 40, PX_TOP - 272);
			[self addChild:maxnums_[0] z:WINLOSE_Z];
			[self addChild:maxnums_[1] z:WINLOSE_Z];

		} else {
			if(g_->is_ready) {
				[self setMascot:g_->mascot byPlayer:false];
				[self setDifficulty:g_->difficulty byPlayer:false];
				g_->is_ready = true;
				[self startRaiseIfReady];
				[opponent_ startRaiseIfReady];
			} else {
				mascotheads_[0] = [CCSprite spriteWithFile: [NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-head.png", 1, 1]];
				mascotheads_[1] = [CCSprite spriteWithFile: [NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-head.png", 2, 2]];
				mascotheads_[2] = [CCSprite spriteWithFile: [NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-head.png", 3, 3]];
				hat_ = [CCSprite spriteWithFile:@"Assets/Graphics/hat.png"];
				hat_.visible = false;
				[self addChild:hat_ z:WINLOSE_Z+1];

				if(opponent_) {
					selectcharacter_ = [CCSprite spriteWithFile:@"Assets/Graphics/select-your-char-vs-label.png"];
				} else {
					selectcharacter_ = [CCSprite spriteWithFile:@"Assets/Graphics/select-your-char-endless-label.png"];
				}
				selectcharacter_.anchorPoint = ccp(0, 0);
				selectcharacter_.position = ccp(PX_LEFT, PX_BOTTOM);
				[self addChild:selectcharacter_ z:WINLOSE_Z];

				mascotheads_[0].position = ccp(PX_LEFT + PX_XSIZE/2 - 110, PX_YSIZE * 0.78);
				mascotheads_[1].position = ccp(PX_LEFT + PX_XSIZE/2, PX_YSIZE * 0.78);
				mascotheads_[2].position = ccp(PX_LEFT + PX_XSIZE/2 + 110, PX_YSIZE * 0.78);
				[self addChild:mascotheads_[0] z:WINLOSE_Z];
				[self addChild:mascotheads_[1] z:WINLOSE_Z];
				[self addChild:mascotheads_[2] z:WINLOSE_Z];
				arrow_[0] = [CCSprite spriteWithFile:@"Assets/Graphics/char-selected-arrow.png"];
				arrow_[1] = [CCSprite spriteWithFile:@"Assets/Graphics/char-selected-arrow.png"];
				arrow_[2] = [CCSprite spriteWithFile:@"Assets/Graphics/char-selected-arrow.png"];
				arrow_[0].position = ccp(50, -15);
				arrow_[1].position = ccp(50, -15);
				arrow_[2].position = ccp(50, -15);
				[mascotheads_[0] addChild:arrow_[0]];
				[mascotheads_[1] addChild:arrow_[1]];
				[mascotheads_[2] addChild:arrow_[2]];
				if([[NSUserDefaults standardUserDefaults] boolForKey:D_ROBOTUNLOCKED] == false) {
					mascotheads_[2].visible = false;
					mascotheads_[0].position = ccp(PX_LEFT + PX_XSIZE/2 - 80, PX_YSIZE * 0.77);
					mascotheads_[1].position = ccp(PX_LEFT + PX_XSIZE/2 + 80, PX_YSIZE * 0.77);
				}

				int maxdepth = [[NSUserDefaults standardUserDefaults] integerForKey:D_DEEPESTREACHED];
				maxdepthrounded_ = (maxdepth / 100) * 100;
				if(scene.layout_ == SINGLEPLAYER && maxdepthrounded_ > 0) {
					speedlabel_ = [CCSprite spriteWithFile:@"Assets/Graphics/set-starting-speed-label.png"];
					speedlabel_.anchorPoint = ccp(0, 0);
					speedlabel_.position = ccp(PX_LEFT, PX_BOTTOM);
					[self addChild:speedlabel_ z:WINLOSE_Z];
					speedarrowleft_ = [CCSprite spriteWithFile:@"Assets/Graphics/speed-arrow-left.png"];
					speedarrowright_ = [CCSprite spriteWithFile:@"Assets/Graphics/speed-arrow-right.png"];
					speedarrowleft_.position = ccp(348, 1024-498);
					speedarrowright_.position = ccp(736, 1024-498);
					[self addChild:speedarrowleft_ z:WINLOSE_Z];
					[self addChild:speedarrowright_ z:WINLOSE_Z];
					for(int i = 0; i < 3; i++) {
						speedbox_[i] = [[[SpeedClippedNode alloc] init] autorelease];
						speedlines_[i] = [CCLabel labelWithString:@" " fontName:@"BPreplay" fontSize:40];
						speedspeed_[i] = [CCLabel labelWithString:@" " fontName:@"BPreplay" fontSize:20];
						speedspeed_[i].position = ccp(0, -40);
						[speedbox_[i] addChild:speedlines_[i]];
						[speedbox_[i] addChild:speedspeed_[i]];
						[self addChild:speedbox_[i] z:WINLOSE_Z];
					}

					[self setSpeedLines:nil];
				}

				if(scene.layout_ == TWOPLAYERS || scene.layout_ == TWOPLAYERSREVERSED) {
					showdifficulty_ = true;
				}
				if(HANDICAPVSAI && scene.layout_ == SINGLEVSAI) {
					showdifficulty_ = true;
				}

				if(showdifficulty_) {
					handicaplabel_ = [CCSprite spriteWithFile:@"Assets/Graphics/set-your-handicap-label.png"];
					handicaplabel_.anchorPoint = ccp(0, 0);
					handicaplabel_.position = ccp(PX_LEFT, PX_BOTTOM);
					difficulties_[0] = [CCLabel labelWithString:@"Fresh Hire" fontName:@"BPreplay" fontSize:40];
					difficulties_[1] = [CCLabel labelWithString:@"Strongman" fontName:@"BPreplay" fontSize:40];
					difficulties_[2] = [CCLabel labelWithString:@"Union Boss" fontName:@"BPreplay" fontSize:40];
					difficulties_[0].position = ccp(PX_LEFT + PX_XSIZE/2, PX_YSIZE * 0.42 + 70);
					difficulties_[1].position = ccp(PX_LEFT + PX_XSIZE/2, PX_YSIZE * 0.42);
					difficulties_[2].position = ccp(PX_LEFT + PX_XSIZE/2, PX_YSIZE * 0.42 - 70);
					[self addChild:handicaplabel_ z:WINLOSE_Z];
					[self addChild:difficulties_[0] z:WINLOSE_Z];
					[self addChild:difficulties_[1] z:WINLOSE_Z];
					[self addChild:difficulties_[2] z:WINLOSE_Z];

					difficultyhighlight_ = [CCSprite spriteWithFile:@"Assets/Graphics/yellow-highlight.png"];
					[self addChild:difficultyhighlight_ z:WINLOSE_Z-1];
				}

				readybutton_ = [CCLabel labelWithString:@"Tap here to start!" fontName:@"BPreplay" fontSize:(scene.layout_ == SINGLEPLAYER ? 50 : 34)];
				int yposready;
				if(scene.layout_ == SINGLEPLAYER) {
					yposready = PX_YSIZE * (speedlabel_ ? 0.2 : 0.6);
				} else {
					yposready = PX_YSIZE * (showdifficulty_ ? 0.2 : 0.6);
				}
				readybutton_.position = ccp(PX_LEFT + PX_XSIZE/2, yposready);
				[self addChild:readybutton_ z:WINLOSE_Z];
				waitinghighlight_ = [CCSprite spriteWithFile:@"Assets/Graphics/yellow-highlight.png"];
				waitinghighlight_.position = readybutton_.position;
				waitinghighlight_.visible = false;
				[self addChild:waitinghighlight_ z:WINLOSE_Z-1];


				[self setMascot:g_->mascot byPlayer:false];
				[self setDifficulty:MEDIUM byPlayer:false];
			}
		}
	}
	return self;
}

- (void) dealloc
{
	[common_ release];
	[blocks_ release];
	[trashqueue_ release];
	[trashiconqueue_ release];
	[chaingroupstats_ release];

	[super cleanup];
	[super removeAllChildrenWithCleanup:false];
	[super dealloc];
}

- (void) setRobotLED:(int)displayno {
	if(!g_->is_ai) return;

	[robotLEDbox_ removeChild:robotLED_[0] cleanup:true];
	[robotLEDbox_ removeChild:robotLED_[1] cleanup:true];
	robotLED_[0] = [CCSprite spriteWithTexture:robotLEDtex_[MIN(displayno / 10, 9)]];
	robotLED_[1] = [CCSprite spriteWithTexture:robotLEDtex_[displayno % 10]];
	robotLED_[0].position = ccp(-20, 0);
	robotLED_[1].position = ccp(20, 0);
	for(int i = 0; i < 2; i++) {
		[robotLEDbox_ addChild:robotLED_[i]];
	}
}

- (void) setMascot:(Mascot)mascot byPlayer:(bool)byplayer {
	int oldmascot = g_->mascot;
	g_->mascot = mascot;
	if (oldmascot != -1) {
		[mascotheads_[oldmascot - 1] setTexture:[[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-head.png", oldmascot, oldmascot]]];
	} else {
		[mascot_ runAction: [CCFadeIn actionWithDuration:0.2]];
		[fingers_ runAction: [CCFadeIn actionWithDuration:0.2]];
	}
	arrow_[0].visible = false;
	arrow_[1].visible = false;
	arrow_[2].visible = false;
	arrow_[mascot - 1].visible = true;

	CGPoint starthat = mascotheads_[oldmascot - 1].position;
	starthat.y += 25;
	CGPoint endhat = mascotheads_[mascot - 1].position;
	endhat.y += 25;
	hat_.position = starthat;
	hat_.visible = true;
	[hat_ stopAllActions];
	[hat_ runAction:[CCSequence actions:
								   [CCJumpTo actionWithDuration:0.2 position:endhat height:50 jumps:1],
								   [CCCallFuncN actionWithTarget:self selector:@selector(endHatJump:)],
														   nil]];
	if(byplayer) {
		[scene playSound:@"Assets/Sounds/hat.caf" volume:0.5];
	}

	if(robotLEDbox_) {
		robotLEDbox_.position = robotLEDPos(0, g_->mirrored);
	}
	if(mascot != ROBOT) {
		robotLEDbox_.visible = false;
	}
		
	fingerstex_[0] = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-pose1-fg.png", mascot, mascot]];
	fingerstex_[1] = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-pose2-fg.png", mascot, mascot]];
	fingerstex_[2] = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-pose3-fg.png", mascot, mascot]];
	fingerstex_[3] = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-pose4-fg.png", mascot, mascot]];

	mascottex_[0] = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-pose1-bg.png", mascot, mascot]];
	mascottex_[1] = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-pose2-bg.png", mascot, mascot]];
	mascottex_[2] = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-pose3.png", mascot, mascot]];
	mascottex_[3] = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-pose4.png", mascot, mascot]];
	[mascot_ setTexture:mascottex_[0]];
	[fingers_ setTexture:fingerstex_[0]];
	CGSize size = [mascottex_[0] contentSize];
	[mascot_ setTextureRect: CGRectMake(0, 0, size.width, size.height)];
	[fingers_ setTextureRect: CGRectMake(0, 0, size.width, size.height)];
	if(g_->mirrored) { fingers_.flipX = true; }
	if(g_->mascot == ROBOT) {
		robotLEDbox_.visible = true;
	}
	shadow_.scaleX = SHADOWSCALE;
	shadow_.scaleY = SHADOWSCALE;

	if(!currentblock_.visible) [self rotateBlockColor];

	[currentblock_ refreshTexture];
	[nextblock_ refreshTexture];
	for(Block* b in blocks_) {
		[b refreshTexture];
	}
	[self updateBackgroundSlices];
}

- (void) setDifficulty:(Difficulty)difficulty byPlayer:(bool)byplayer {
	int olddifficulty = g_->difficulty;
	g_->difficulty = difficulty;

	if(olddifficulty != -1) {
		[difficulties_[olddifficulty] setColor:ccc3(0xFF,0xFF,0xFF)];
	}
	[difficulties_[difficulty] setColor:ccc3(0,0,0)];
	difficultyhighlight_.position = difficulties_[difficulty].position;

	if(byplayer && olddifficulty != difficulty) {
		[scene playSound:@"Assets/Sounds/click.caf" volume:1.0];
	}
}

- (void) setSpeedLines:(id)sender {
	if(!speedlabel_) return;

	selecteddepth_ = newselecteddepth_;

	[speedbox_[0] stopAllActions];
	[speedbox_[1] stopAllActions];
	[speedbox_[2] stopAllActions];

	speedbox_[0].position = PREVSPEEDPOS;
	speedbox_[1].position = SPEEDPOS;
	speedbox_[2].position = NEXTSPEEDPOS;

	displaylines_[0] = selecteddepth_ - 100;
	if(displaylines_[0] < 0) displaylines_[0] = maxdepthrounded_;
	displaylines_[1] = selecteddepth_;
	displaylines_[2] = selecteddepth_ + 100;
	if(displaylines_[2] > maxdepthrounded_) displaylines_[2] = 0;

	for(int i = 0; i < 3; i++) {
		ccTime raisetime = [self raiseTime:displaylines_[i]];
		int lpm = 60.0 / raisetime;
		[speedlines_[i] setString:[NSString stringWithFormat:@"%d Lines per Min.", lpm]];
		[speedspeed_[i] setString:(displaylines_[i] == 0) ? @"(Default)" : [NSString stringWithFormat:@"(Unlocked at %d Lines Cleared)", displaylines_[i]]];
	}
}

- (void) moveSpeedLines:(bool)prev {
	if(!speedlabel_) return;

	if(selecteddepth_ != newselecteddepth_) return;

	newselecteddepth_ = selecteddepth_ + (prev ? -100 : 100);
	if(newselecteddepth_ < 0) newselecteddepth_ = maxdepthrounded_;
	if(newselecteddepth_ > maxdepthrounded_) newselecteddepth_ = 0;
	int sideindex;
	CGPoint targetpos;
	if(prev) {
		sideindex = 0;
		targetpos = NEXTSPEEDPOS;
	} else {
		sideindex = 2;
		targetpos = PREVSPEEDPOS;
	}

	[speedbox_[1] runAction:[CCSequence actions:
				[CCMoveTo actionWithDuration:0.5 position:targetpos],
				[CCCallFuncN actionWithTarget:self selector:@selector(setSpeedLines:)],
				nil]];
	[speedbox_[sideindex] runAction:[CCSequence actions:
				[CCMoveTo actionWithDuration:0.5 position:SPEEDPOS],
				nil]];

	[scene playSound:@"Assets/Sounds/click.caf" volume:1.0];
}

- (void) startRaiseIfReady {
	if(ingamestate_ == MASCOTSELECT && [self isReadyToStart] && (opponent_ == nil || [opponent_ isReadyToStart])) {
		[self removeChild:stealjob_ cleanup:true];
		[self removeChild:maxbotlevel_ cleanup:true];
		[self removeChild:maxnums_[0] cleanup:true];
		[self removeChild:maxnums_[1] cleanup:true];
		[self removeChild:selectcharacter_ cleanup:true];
		[self removeChild:mascotheads_[0] cleanup:true];
		[self removeChild:mascotheads_[1] cleanup:true];
		[self removeChild:mascotheads_[2] cleanup:true];
		[self removeChild:hat_ cleanup:true];
		if(showdifficulty_) {
			[self removeChild:handicaplabel_ cleanup:true];
			[self removeChild:difficulties_[0] cleanup:true];
			[self removeChild:difficulties_[1] cleanup:true];
			[self removeChild:difficulties_[2] cleanup:true];
			[self removeChild:difficultyhighlight_ cleanup:true];
		}
		[self removeChild:readybutton_ cleanup:true];
		[self removeChild:waitinghighlight_ cleanup:true];

		if(scene.layout_ == SINGLEPLAYER) {
			selecteddepth_ = newselecteddepth_;
			highestblocklevel_ = -selecteddepth_;
			[self removeChild:speedlabel_ cleanup:true];
			[self removeChild:speedarrowleft_ cleanup:true];
			[self removeChild:speedarrowright_ cleanup:true];
			[self removeChild:speedbox_[0] cleanup:true];
			[self removeChild:speedbox_[1] cleanup:true];
			[self removeChild:speedbox_[2] cleanup:true];
		}
		[self schedule: @selector(initialRaiseAnimation:) interval:OPENINGRISEDELAY];
		ingamestate_ = RAISING;
	}
}


- (void) initialRaiseAnimation: (ccTime) dt {
	if(raiselevel_ == 0) {
		if(MUSIC && scene->playinginterstitial_) {
			[self runAction:[CCSequence actions:
				[CCDelayTime actionWithDuration:COUNTDOWNSOUNDDELAY],
			    [CCCallFuncN actionWithTarget:self selector:@selector(playCountdown:)],
																   nil]];
			scene->playinginterstitial_ = false;
			switch(scene.layout_) {
				case SINGLEPLAYER:
					switch(g_->mascot) {
						case KEITH:
							[scene playMusic:@"Assets/Music/Brate.m4a"];
							break;
						case BEAR:
							[scene playMusic:@"Assets/Music/Botany.m4a"];
							break;
						case ROBOT:
							[scene playMusic:@"Assets/Music/Dark Charge.m4a"];
							break;
					}
					break;
				case TWOPLAYERS:
				case TWOPLAYERSREVERSED:
					[scene playMusic:@"Assets/Music/T.T..m4a"];
					break;
				case SINGLEVSAI:
					[scene playMusic:@"Assets/Music/roove,maxamp+2.m4a"];
					break;
			}
			[scene pauseMusic];
		}

		if(scene.layout_ != SINGLEPLAYER) {
			int msg = 0;
			if([self mascot] == [opponent_ mascot] && g_->playerno > 0) {
				msg = 1;
			}
			[self speechBubble:C_TTBEGIN([self mascot], [opponent_ mascot], msg)];
		} else {
			[self addGoalLine:0];
		}

		CCLabel* ready = [CCLabel labelWithString:@"READY..."
		                           fontName:@"Franchise"
								   fontSize:80];
		readylabel_ = ready;
		CGPoint pos = WINLABELPOS;
		pos.y = -100;
		ready.position = pos;
		ready.color = ccc3(255,255,255);
		[self addChild:ready z:WINLOSE_Z];
		[ready runAction:[CCSequence actions:
						   [CCMoveTo actionWithDuration:OPENINGRISEDELAY * (STARTRAISEHEIGHT+1) position:WINLABELPOS],
						   [CCDelayTime actionWithDuration:STARTDELAY - CARDFLIPTIME],
						   [CCOrbitCamera actionWithDuration:CARDFLIPTIME radius:1 deltaRadius:0 angleZ:0 deltaAngleZ:90 angleX:0 deltaAngleX:0],
						   [CCCallFuncN actionWithTarget:self selector:@selector(startGame:)],
																   nil]];

		if(g_->is_ai) [ready setOpacity:0];
	}
	if(raiselevel_ <= -STARTRAISEHEIGHT) {
		[self unschedule:@selector(initialRaiseAnimation:)];
		if(!g_->is_ai) {
			[foreground_ runAction:[CCSequence actions:
											   [CCDelayTime actionWithDuration:STARTDELAY],
											   [CCFadeTo actionWithDuration:0.3 opacity:0],
																	   nil]];
		}
	} else {
		[self raiseField:OPENINGRISEDELAY];
	}
}

- (void) playCountdown: (ccTime) dt {
	[scene playSound:@"Assets/Sounds/countdown.caf" volume:1.0];
}

- (void) startGame: (ccTime) dt {
	[readylabel_ setString:@"WORK!"];
	id orbit = [CCOrbitCamera actionWithDuration:CARDFLIPTIME radius:1 deltaRadius:0 angleZ:270 deltaAngleZ:90 angleX:0 deltaAngleX:0];
	if(MUSIC) {
		[scene resumeMusic];
	}
	
	[readylabel_ runAction:[CCSequence actions:
										orbit,
									   [CCDelayTime actionWithDuration:1.0],
									   [CCMoveTo actionWithDuration:0.3 position:g_->winstartpos],
									   [KillNodeAction action],
															   nil]];

	[self speechBubble:nil];

	ingamestate_ = PLAY;

	if(g_->difficulty == HARD) {
		int trashamounts[3];
		trashamounts[0] = 0;
		trashamounts[1] = 4;
		trashamounts[2] = 0;
		[self receiveTrash:trashamounts chaingroup:-2];
		[self dropTrashFromQueue];
	}

	[self schedule: @selector(gameLoop:) interval:FRAMETIME];
}

- (void) gameLoop: (ccTime) dt {
	// Split up evaluation at low framerates to prevent blocks from falling into each other
#define MAX_FRAME_TIME (0.95/FALLSPEED)
	if(dt > MAX_FRAME_TIME) {
		[self gameLoop:(dt - MAX_FRAME_TIME)];
		dt = MAX_FRAME_TIME;
	}
	gamecounter_ += dt;
	if(floor(gamecounter_ - dt) < floor(gamecounter_)) [self updateTimer];

	// Sort blocks from bottom to top.  This helps make the falling logic consistent.
	[blocks_ sortUsingFunction:isHigherThan context:nil];

	bool existsfalling = false, existsmelting = false;
	Block* block;
	// Loop through falling blocks and make them go downwards.
	for(block in blocks_) {
		if(block.isfalling) {
			existsfalling = true;

			CGPoint lastpos = block.gridpos;
			block.falldelay -= dt;
			if(block.falldelay < 0) {
				// Make the block go downwards.
				block.ishanging = false;
				CGPoint currentpos = lastpos;
				currentpos.y += block.falldelay * FALLSPEED;
				block.falldelay = 0;
				[block setGridpos:currentpos];

				// Has it collided with a block that's hanging in the air due to a recent placement?
				// (as distinct from a landed one)
				// If so, revert to the previous position.  This should effectively hold in place blocks
				// that are above this one too.
				bool hangingstall = false;
				for(Block* b2 in blocks_) {
					if(b2 != block && b2.isfalling && CGRectIntersectsRect([block gridRect], [b2 gridRect])) {
						hangingstall = true;
						break;
					}
				}
				if(hangingstall) {
					[block setGridpos:lastpos];
					continue;
				}

			}

			// Land blocks onto the stack.
		    if(block.isfalling && !block.ishanging) { [self tryLand: block]; }
			CGPoint currentpos = block.gridpos;
			// If a trash block fell below the top-of-screen threshold, then remove it from the icon list.
			if(block.col == TRASH && currentpos.y < GRID_YSIZE && lastpos.y >= GRID_YSIZE) {
				[self dropTrashIconForBlock:block];
			}
		}

	}

	// Loop through melting blocks
RESTART:
	for(block in blocks_) {
		if(block.ismelting) {
			existsmelting = true;
			block.melttime += dt;
			if(block.melttime >= MELTTIME) {
				// If the melting animation is finished, make it disappear.  The iterator is invalidated from this,
				// so start over.
				[self endMelt:block];
				goto RESTART;
			}
		}
	}
	
	// Sync the grid array with the list of landed blocks, then use that information to check for new heart matches
	[self syncGrid];
	[self testMatches];

	// make click sounds if no matches
	if(numlandedthisframe_ > 0 && lastblockdroppedtime_ < gamecounter_ - 0.1) {
		[scene playSound:@"Assets/Sounds/thud2.caf" volume:0.25];
		lastblockdroppedtime_ = gamecounter_;
	}
	numlandedthisframe_ = 0;

	// Check if there's any active blocks, and also make sure inactive landed blocks have zero chaincount.
	exists_active_block_ = false;
	bool exists_thrown_block = false;
	for(block in blocks_) {
		if(!block.ismelting && !block.isfalling) {
			block.chaincount = 0;
			block.chaingroup = -1;
		} else {
			exists_active_block_ = true;

			if(block.isbeingthrown) {
				exists_thrown_block = true;
			}
		}
	}

	bool isdying = [self isDying:0];
	// Sort blocks from bottom to top again.  This helps make the dying prediction logic consistent.
	[blocks_ sortUsingFunction:isHigherThan context:nil];
	bool isdyingafterstabilize = [self predictDyingAfterStabilize];

	[self dropTrashFromQueue];

	[self computeChainStats];

	// Figure out how far down the stack is relative to the blocks.
	int highestblock = 0;
	for(Block *b in blocks_) {
		CGRect rect = [b gridRect];
		int height = CGRectGetMaxY(rect) + 1;
		if(height > highestblock) highestblock = height;
	}
	// Show tip on how to raise stack if it's low enough.
	int manuallyraised = [[NSUserDefaults standardUserDefaults] integerForKey:D_LINESMANUALLYRAISED];
	if(highestblock <= 6 && manuallyraised < 10 && !g_->is_ai && !placehint_ && !secondtoucheveractive_) {
#if !NORAISEHINT
		raisehint_ = true;
		[self speechBubble:C_RAISEHINT];
		raisefingers_[0].visible = true;
		raisefingers_[1].visible = true;
#endif
	} else {
		if(raisehint_) {
			raisehint_ = false;
			[self speechBubble:nil];
			raisefingers_[0].visible = false;
			raisefingers_[1].visible = false;
		}
	}
	int oldlevel = highestblocklevel_;
	int newlevel = highestblock + raiselevel_ - selecteddepth_;
	if(oldlevel > newlevel) {
		highestblocklevel_ = newlevel;
		[self setDepthString];
	}
	
	// Determine whether the player looks like he needs the placement tutorial
	bool showhint = false;
	if(ALWAYSTUTORIAL) showhint = true;
	hintcounter_ += dt;
	if(hintcounter_ >= 10 && heartscleared_ < 10 && !g_->is_ai && g_->mirrored || ALWAYSTUTORIAL) showhint = true;
	if(heartscleared_ <= 5) {
		if(heartsrecord_ <= 5 && !g_->is_ai && g_->mirrored) showhint = true;
	}
	if(showhint) {
		placehint_ = true;
		[self speechBubble:C_PLACEHINT];
		[self placeTutorialBlock];
	}


	// Raise the stack from the bottom.
	if(INFINITEMODE || !isdying) {
		raisecounter_ += dt;
		ccTime raisetime = [self raiseTime:MAX(0, -highestblocklevel_)];
		if(raisecounter_ >= raisetime) {
			raisecounter_ -= raisetime;
			[self raiseField:RISEDELAY];
		}
	}

	// Make AI move
	if(g_->is_ai) {
		aicounter_ += dt;
		ccTime aitime = [self aiTime];
		if(aicounter_ >= aitime) {
			aicounter_ -= aitime;
			[self aiPlay:false];
		} else if(aicounter_ >= aitime - 0.5) {
			[self crouch];
		}
	}


	// Show the preview block (which may have become validated/invalidated by the changed in the stack)
	[self placePreview];

#if ROBOTMAXED
	// Test explosion animation
	if(g_->mascot == ROBOT) {
		dangercountzero_ = true;
		[self die];
	}
#endif

	// Check for 3,2,1 and game overs.
	if(isdying && isdyingduetofalling_ == false) {
		if(!won_) {
			[self incrementDangerCounter: dt];

			if(dangercountzero_) {
				if(!exists_thrown_block && isdyingafterstabilize) {
					[self die];
				}
			}
		}
	} else {
		dangercounter_ = 0;
		dangercountzero_ = false;
		if(dangersign_) {
			[self removeChild:dangersign_ cleanup:true];
			dangersign_ = nil;
			vacationsquare_.visible = true;
			vacationlabel_.visible = true;
			vacationtimelabel_.visible = true;

		}
		if(heartsbeating_) {
			// stop heartbeat
			for(block in blocks_) {
				[block->heart1 stopAllActions];
				[block->heart2 stopAllActions];
				[block refreshTexture];
			}
			heartsbeating_ = false;
			[self unschedule:@selector(alarmSoundCallback:)];
			[scene stopSound:alarmsoundid_];
		}
	}
}

- (void) updateTimer {
	if(!layoutname_ || g_->timerpos.x == -1 || won_ || lost_) return;
	int mins = gamecounter_ / 60;
	int secs = gamecounter_ - (mins * 60);
	NSString* timestr = [NSString stringWithFormat:@"%02d:%02d", mins, secs];
	int xdiff = 7;
	int colonxdiff = 7;
	int colonydiff = 9;
	int xpos = 21, ypos = 12;
	if(scene.layout_ == TWOPLAYERSREVERSED) {
		xdiff = 3;
		colonxdiff = 2;
		colonydiff = 4;
		xpos = ypos = 6;
	}
	for(int i = 0; i < 5; i++) {
		[timerbg_ removeChild:timerdigits_[i] cleanup:true];
		unichar c = [timestr characterAtIndex:i];
		NSString* filename;
		if(c == ':') {
			filename = [NSString stringWithFormat:@"Assets/Graphics/%s/timer-colon.png", layoutname_];
		} else {
			filename = [NSString stringWithFormat:@"Assets/Graphics/%s/timer-num-%C.png", layoutname_, c];
		}
		timerdigits_[i] = [CCSprite spriteWithFile:filename];
		timerdigits_[i].position = ccp(xpos, (ypos + (c == ':' ? colonydiff : 0)));
		timerdigits_[i].anchorPoint = ccp(0, 0);
		[timerbg_ addChild:timerdigits_[i]];
		xpos += [timerdigits_[i] contentSize].width;
		xpos += (c == ':' ? colonxdiff : xdiff);
	}

	// blink colon
//	timerdigits_[2].visible = !(secs % 2);
}
 
- (void) endMelt:(Block*) block {
	// If this is a trash icon living above the board, get rid of its icon.
	[self dropTrashIconForBlock:block];

	int chaincount = block.chaincount;
	int chaingroup = block.chaingroup;

	// remove block from "blocks" NSMutableArray
	[blocks_ removeObject:block];
	[self syncGrid];

	[blocksdisplay_ removeChild:block cleanup:true];

	[self checkUnsupported:chaincount chaingroup:chaingroup];
}

- (void) checkUnsupported:(int) chaincount chaingroup:(int) chaingroup {
	// loop from bottom to top to make all blocks supported by it start falling
	int x, y, bx, by;
	int index;

	// TODO: fix bug where there's a small gap between two blocks that should fall at the same time

	for(y = 0; y < GRID_YMAX; y++) {
		for(x = 0; x < GRID_XSIZE; x++) {
			index = y * GRID_XMAX + x;
			if(index < 0 || index >= GRID_XMAX * GRID_YMAX) continue;
			Block* b = blockgrid_[index];
			if(b.ismelting) continue;
			CGPoint gridpos = b.gridpos;
			by = gridpos.y;
			if(by <= 0) continue;
			BOOL issupported = false;
			for(bx = gridpos.x; bx < gridpos.x + [b xSize]; bx++) {
				index = (by-1) * GRID_XMAX + bx;
				if(index < 0 || index >= GRID_XMAX * GRID_YMAX) { if(INFINITEMODE) { issupported = true; } continue; }
				Block* supportblock = blockgrid_[index];
				if(supportblock && !supportblock.isfalling) issupported = true;
			}
			if(!issupported) { 
				[b startFalling:chaincount chaingroup:chaingroup delay:FALLDELAY];
			}
		}
	}
}

- (void) tryLand:(Block*) block {
	BOOL landed = false;
	float landingypos;
	Block* b2;
	
	// Check for falling blocks colliding with landed blocks.  
	// Trying blocks from top to bottom helps avoid collision glitches
	for(int i = [blocks_ count] - 1; i >= 0; i--) {
		b2 = [blocks_ objectAtIndex:i];
		CGRect supportrect = [b2 gridRect];
		if(!b2.isfalling && CGRectIntersectsRect([block gridRect], supportrect)) {
			landed = true;
			landingypos = [b2 ySize] + b2.gridpos.y;
			break;
		}
	}

	// Try the ground last
	if(!landed && block.gridpos.y <= 0.0) {
		landed = true;
		landingypos = 0;
	}
	
	if(landed) {
		block.isfalling = false;
		CGPoint gridpos = block.gridpos;
		gridpos.y = landingypos;
		block.gridpos = gridpos;

		// shake effect for trash landing
		if(block.col == TRASH) {
			if(block.neverlanded) {
				[self runAction:[CCSequence actions:
											   [CCMoveBy actionWithDuration:0.05 position:ccp(10,10)],
											   [CCMoveBy actionWithDuration:0.05 position:ccp(-20,-20)],
											   [CCMoveBy actionWithDuration:0.05 position:ccp(15,15)],
											   [CCMoveBy actionWithDuration:0.05 position:ccp(-10, -10)],
											   [CCMoveTo actionWithDuration:0.05 position:ccp(g_->offsetx,0)],
																	   nil]];
				// Have a cap to avoid playing too many sounds at a time to
				// avoid lag
				if(lasttrashdroppedtime_ < gamecounter_ - 0.1) {
					[scene playSound:@"Assets/Sounds/trash.caf" volume:1.0];
					lasttrashdroppedtime_ = gamecounter_;
				}

			}

			[self checkAroundTrash: block];
		} else {
			numlandedthisframe_++;
		}
			

		block.neverlanded = false;
	}
}

- (void) checkAroundTrash:(Block*) t {
	int x, y, i;
	for(x = t.gridpos.x - 1; x < t.gridpos.x + [t xSize] + 1; x++) {
		for(y = t.gridpos.y - 1; y < t.gridpos.y + [t ySize] + 1; y++) {
			i = y * GRID_XMAX + x;
			if(i >= 0 && i < GRID_XMAX * GRID_YMAX) {
				if(blockgrid_[i] && blockgrid_[i].ismelting && blockgrid_[i].col != TRASH && blockgrid_[i].melttime < GENEROUSTRASHMELTTIME) {
					[t startMelt:blockgrid_[i].chaincount chaingroup:blockgrid_[i].chaingroup];
					return;
				}
			}
		}
	}
}

- (void) syncGrid {
	int i;
	for(i = 0; i < GRIDTILES; i++) {
		covergrid_[i] = true;
		blockgrid_[i] = nil;
		heartgrid_[i] = 0;
	}

	// covergrid_ defaults to false inside playable area, true outside area
	for(int x = 0; x < GRID_XSIZE; x++) {
		for(int y = 0; y < GRID_YSIZE; y++) {
			i = y * GRID_XMAX + x;
			covergrid_[i] = false;
		}
	}
	
	int x;
	int y;
	for(Block* b in blocks_) {
		// Find out what tiles any block covers for placement purposes (even a pixel of coverage is enough to count)
		for(x = b.gridpos.x; x < b.gridpos.x + [b xSize]; x++) {
			for(y = floor(b.gridpos.y); y < ceil(b.gridpos.y + [b ySize]); y++) {
				i = y * GRID_XMAX + x;
				if(i >= 0 && i < GRID_XMAX * GRID_YMAX)
					covergrid_[y * GRID_XMAX + x] = true;
			}
		}


		if(b.isfalling) continue; // can't match with a falling block
				
		// Map each tile of each block into the blockgrid
		for(x = b.gridpos.x; x < b.gridpos.x + [b xSize]; x++) {
			for(y = b.gridpos.y; y < b.gridpos.y + [b ySize]; y++) {
				i = y * GRID_XMAX + x;
				if(i >= 0 && i < GRID_XMAX * GRID_YMAX)
					blockgrid_[y * GRID_XMAX + x] = b;
			}
		}

		if(b.ismelting) continue; // can't use heart of already melted block
		
		// Map the position of the hearts for regular blocks
		if(b.shape & LEFT || b.shape & DOWN) {
			x = b.gridpos.x;
			y = b.gridpos.y;
			i = y * GRID_XMAX + x;
			if(i >= 0 && i < GRID_XMAX * GRID_YMAX)
				heartgrid_[y * GRID_XMAX + x] = b.col;
		}
		if(b.shape & UP) {
			x = b.gridpos.x;
			y = b.gridpos.y + 2;
			i = y * GRID_XMAX + x;
			if(i >= 0 && i < GRID_XMAX * GRID_YMAX)
				heartgrid_[y * GRID_XMAX + x] = b.col;
		}
		if(b.shape & RIGHT) {
			x = b.gridpos.x + 2;
			y = b.gridpos.y;
			i = y * GRID_XMAX + x;
			if(i >= 0 && i < GRID_XMAX * GRID_YMAX)
				heartgrid_[y * GRID_XMAX + x] = b.col;
		}
	}
}

- (void) testMatches {
	int x;
	int y;
	int checker;
	int combocount = 0;
	int chaincount = 0;
	int chaingroup = -1;
	int blockscleared = 0;
	bool playedsound = false;
	Block* b1 = nil;
	Block* b2 = nil;
	for(x = 0; x < GRID_XSIZE; x++) {
		for(y = 0; y < GRID_YSIZE; y++) {
			for(checker = 0; checker < 2; checker++) {  // check right block if 0, up block if 1
				int cur = y * GRID_XMAX + x;  // current tile being checked
				int up = (y+1) * GRID_XMAX + x;  // tile to the top of that
				int right = y * GRID_XMAX + (x+1);  // tile to the right of that
				int other = (checker == 0) ? right : up;  // alternate between which to compare to
				
				if(heartgrid_[cur] == EMPTY || heartgrid_[cur] == TRASH) {
					// No hearts here, check something else.
					continue;
				}
				
				// Is there a match?
				if(heartgrid_[cur] == heartgrid_[other]) {
					b1 = blockgrid_[cur];
					b2 = blockgrid_[other];
				} else { continue; }
				
				// Compute chains and combos.
				// - Combo counting is simple, it's just the number of hearts that was cleared in a single frame.
				// - Chaining is more elaborate.  Each block keeps track of its "chaincount".  
				// Landed, nonmelting blocks always have a chaincount of zero.  When a block melts, its chaincount is
				// incremented.  When a landed block starts falling as a result of becoming unsupported, it gets a copy
				// of the chaincount from the melted block that caused it to fall.  If at the moment of landing
				// it then matches another block, then its chaincount is copied to the other block.  The net effect
				// is to keep track of causality.  Each individual match in the chain
				// contributes an attack in its own right (starting at when chaincount = 2), increasing the number
				// of attacks quadratically, but the attack is only unleashed when both fields cease to have any
				// melting/falling blocks.
				chaincount = 0;
				if(!b1.ismelting && b1.chaincount > chaincount) { chaincount = b1.chaincount; }
				if(!b2.ismelting && b2.chaincount > chaincount) { chaincount = b2.chaincount; }		
				// Identify blocks in a single conceptual chain using a unique ID, for purposes of knowing
				// when to drop trash on the opposing player (which we do when the chain is 100% finished).
				if(chaingroup == -1) {
					if(b1.chaingroup != -1) {
						chaingroup = b1.chaingroup;
					} else if(b2.chaingroup != -1) {
						chaingroup = b2.chaingroup;
					} else {
						// If this match isn't part of any chaingroup, create a new one based on the number of hearts
						// cleared (an arbitrary unique GUID).
						chaingroup = heartscleared_;
					}
				}
				chaincount++;
				if(!b1.ismelting) blockscleared++;
				if(!b2.ismelting) blockscleared++;
				combocount += [b1 startMelt:chaincount chaingroup:chaingroup];
				combocount += [b2 startMelt:chaincount chaingroup:chaingroup];
				blockscleared_ += blockscleared;
				if(blockscleared > 0) {
					NSNumber* groupnum = [NSNumber numberWithInt:chaingroup];
					ChaingroupStats* stats = [chaingroupstats_ objectForKey:groupnum];
					if(stats == nil) {
						stats = [[[ChaingroupStats alloc] init] autorelease];
					}
					stats->numblocks_ += blockscleared;
					if(chaincount < MAX_CHAIN) {
						stats->numchains_[chaincount] += 1;
					}

					[chaingroupstats_ setObject:stats forKey:groupnum];
				}
				blockscleared = 0;
				[self doAttack:chaincount chaingroup:chaingroup iscombo:false b1:b1 b2:b2];

				if(chaincount > 1 && !playedsound) {
					[scene playSound:[NSString stringWithFormat:@"Assets/Sounds/chain%02d.caf", MIN(chaincount, 12)] volume:1.0];
					playedsound = true;
				}

				// Now to clear out any nearby trash blocks.
				int xtocheck[6];
				int ytocheck[6];
				int k = 0;
				Block* b;

				// Make a list of the newly exploded tiles to check around.  Because we're matching only 2 blocks
				// at a time here, there can be no more than 6 tiles to check around.
				for(b = b1;; b = b2, k = 3) {
					CGPoint gridpt = b.gridpos;
					xtocheck[k] = gridpt.x;
					ytocheck[k] = gridpt.y;
					if(b.shape & LEFT || b.shape & RIGHT) {
						xtocheck[k + 1] = xtocheck[k] + 1;
						xtocheck[k + 2] = xtocheck[k] + 2;
						ytocheck[k + 1] = ytocheck[k];
						ytocheck[k + 2] = ytocheck[k];
					} else if(b.shape & UP || b.shape & DOWN) {
						ytocheck[k + 1] = ytocheck[k] + 1;
						ytocheck[k + 2] = ytocheck[k] + 2;
						xtocheck[k + 1] = xtocheck[k];
						xtocheck[k + 2] = xtocheck[k];
					}
					if(b == b2) break;
				}
				
				// Now melt any trash above, below, left or right of these 6 tiles.
				int tx, ty, index;
				for(k = 0; k < 6; k++) {
					tx = xtocheck[k];
					ty = ytocheck[k];
					index = ty * GRID_XMAX + (tx-1); if(tx > 0 && index >= 0 && index < GRID_XMAX * GRID_YMAX && blockgrid_[index].col == TRASH) { [ blockgrid_[index] startMelt:chaincount chaingroup:chaingroup]; } // LEFT
					index = (ty-1) * GRID_XMAX + tx; if(ty > 0 && index >= 0 && index < GRID_XMAX * GRID_YMAX && blockgrid_[index].col == TRASH) { [ blockgrid_[index] startMelt:chaincount chaingroup:chaingroup]; } // DOWN
					index = ty * GRID_XMAX + (tx+1); if(index >= 0 && index < GRID_XMAX * GRID_YMAX && blockgrid_[index].col == TRASH) { [ blockgrid_[index] startMelt:chaincount chaingroup:chaingroup]; } // RIGHT
					index = (ty+1) * GRID_XMAX + tx; if(index >= 0 && index < GRID_XMAX * GRID_YMAX && blockgrid_[index].col == TRASH) { [ blockgrid_[index] startMelt:chaincount chaingroup:chaingroup]; } // UP
				}
 
			}
		}
	}
	
	// Score the combo after everything else, to take account of every heart matched during this frame.
	[self doAttack:combocount chaingroup:chaingroup iscombo:true b1:b1 b2:b2];

	if(!playedsound) {
		if(combocount > 2) {
			[scene playSound:@"Assets/Sounds/chain00.caf" volume:1.0];
			playedsound = true;
		} else if(combocount == 2) {
			[scene playSound:@"Assets/Sounds/chain01.caf" volume:1.0];
			playedsound = true;
		}
	}

	if(playedsound) {
		// disable land-thud sound
		numlandedthisframe_--;
	}
}

+ (CCSprite*) comboCircleWithCount:(int)count pos:(CGPoint)pos {
	CCSprite* circle = [CCSprite spriteWithFile:@"Assets/Graphics/combo.png"];
	circle.position = pos;
	[circle setOpacity:0xC0];
	CCLabel* label1 = [CCLabel labelWithString:[NSString stringWithFormat:@"+%d", count]
		fontName:@"BPreplay" fontSize:20];
	label1.position = ccp(60 / 2, 60 / 2);
	label1.color = ccc3(0xAA, 0xAA, 0xAA);
//	label1.color = chainColor(count - 3);
	circle.color = chainColor(count - 2);
	label1.anchorPoint = ccp(0.5, 0.5);
	[circle addChild:label1];
	[circle runAction:[CCSequence actions:
								   [CCDelayTime actionWithDuration:0.5],
								   [CCFadeTo actionWithDuration:0.3 opacity:0],
								   [KillNodeAction action],
														   nil]];

	return circle;
}

+ (CCSprite*) chainStarWithCount:(int)c start:(CGPoint)starstart end:(CGPoint)jumpend {
	CCSprite* star = [CCSprite spriteWithFile:@"Assets/Graphics/chain-star-ds.png"];
	
	star.position = starstart;
	star.anchorPoint = ccp(0.5, 0.5);
	[star setOpacity:0xC0];

	CGSize starsize = [star contentSize];
	CCSprite* innerstar = [CCSprite spriteWithFile:@"Assets/Graphics/chain-star.png"];
	[innerstar setColor:chainColor(c - 2)];
	innerstar.position = ccp(starsize.width / 2, starsize.height / 2);
	innerstar.anchorPoint = ccp(0.5, 0.5);
	[star addChild:innerstar];

	CCLabel* label1 = [CCLabel labelWithString:[NSString stringWithFormat:@"x%d", c]
		fontName:@"BPreplay" fontSize:25];
	label1.position = ccp(starsize.width / 2, starsize.height / 2);
	label1.color = ccc3(0,0,0);
	label1.anchorPoint = ccp(0.5, 0.5);
	[star addChild:label1 z:2];

	[star runAction:[CCSequence actions:
								   [CCDelayTime actionWithDuration:0.7],
								   [CCJumpTo actionWithDuration:1 position:jumpend height:200 jumps:1],
								   [KillNodeAction action],
														   nil]];
	[star runAction:[CCSequence actions:
								   [CCDelayTime actionWithDuration:0.5],
								   [CCRotateBy actionWithDuration:1 angle:360],
														   nil]];

	return star;
}

+ (CGPoint) iconPosIsCombo:(bool)iscombo block1:(Block*)b1 block2:(Block*)b2 pxxsize:(int)pxxsize pxysize:(int)pxysize {
	CGPoint midpos = ccp(([b1 position].x + [b2 position].x) / 2,
			             ([b1 position].y + [b2 position].y) / 2);
	int iconborder;
	// slightly different positioning policy to avoid overlapping chain and combo reports
	if(iscombo) {
		midpos.x += 50;
		midpos.y += 50;
		iconborder = 50;
	} else {
		midpos.x -= 50;
		midpos.y -= 50;
		iconborder = 100;
	}
	midpos.x = MAX(midpos.x, iconborder);
	midpos.x = MIN(midpos.x, pxxsize - iconborder);
	midpos.y = MAX(midpos.y, iconborder);
	midpos.y = MIN(midpos.y, pxysize - iconborder);

	return midpos;
}

- (void) doAttack:(int)count chaingroup:(int)chaingroup iscombo:(bool)iscombo b1:(Block*)b1 b2:(Block*)b2 {
	int oldheartscleared = heartscleared_;
	if(iscombo) {
		heartscleared_ += count;
		if(!g_->is_ai) {
			int heartsrecord = [[NSUserDefaults standardUserDefaults] integerForKey:D_HEARTSCLEARED];
			heartsrecord += count;
			[[NSUserDefaults standardUserDefaults] setInteger:heartsrecord forKey:D_HEARTSCLEARED];
		}
	}

	if(b1 == nil || b2 == nil) return;
	if(count == 0) return;

	CGPoint midpos = [PuzzleStack iconPosIsCombo:iscombo block1:b1 block2:b2 pxxsize:PX_XSIZE pxysize:PX_YSIZE];
	CGPoint starstart = [self convertToNodeSpace:[blocksdisplay_ convertToWorldSpace:midpos]];

	int trashamounts[3];
	trashamounts[0] = trashamounts[1] = trashamounts[2] = 0;

	if(iscombo) {
		if(!g_->is_ai) {
			if(count < MAX_CHAIN) {
				numcombos_[count]++;
				int record = [[NSUserDefaults standardUserDefaults] integerForKey:D_COMBOCOUNT(count)];
				record++;
				[[NSUserDefaults standardUserDefaults] setInteger:record forKey:D_COMBOCOUNT(count)];
			}
		}
		if((heartscleared_ / HEARTS_FOR_TOKEN_TRASH) > (oldheartscleared / HEARTS_FOR_TOKEN_TRASH)) {
			trashamounts[0] += 1;
		}

		if(count > 2) {
			int trashsize = (g_->difficulty == HARD) ? 0 : 1;
			trashamounts[trashsize] += count - 2;

			[self addChild:[PuzzleStack comboCircleWithCount:count pos:starstart] z:CHAIN_Z];
		}

	} else {  // is a chain
		static int starcycle = 0;
		starcycle++;
		int c = ALWAYSSTAR ? starcycle : count;
		if(!ALWAYSSTAR && c < 2) return;
		int record = [[NSUserDefaults standardUserDefaults] integerForKey:D_SPECIALCLEARED];
		record += count;
		[[NSUserDefaults standardUserDefaults] setInteger:record forKey:D_SPECIALCLEARED];

		CGPoint jumpend = g_->winstartpos;
		jumpend.y -= 500;
		[self addChild:[PuzzleStack chainStarWithCount:c start:starstart end:jumpend] z:CHAIN_Z];

		switch(count) {
			case 1:
				break;
			case 2:
				trashamounts[1] += 1;
				if(g_->difficulty == EASY) {
					trashamounts[1] += 1;
				}
				break;
			case 3:
				trashamounts[2] += 1;
				break;
			case 4:
				trashamounts[2] += 2;
				break;
			default:
				trashamounts[2] += 3;
				break;
		}

	}

	if(opponent_) {
#if CANCELTRASH
		// Cancel trash from incoming attack.  Cancel starting from the most
		// newly arrived icons, according to the total area of the current
		// attack, rounding up for the last icon (so a size-1 attack can
		// counter incoming trash of size 3 on the margin).
		int cancelsize = 0;
		cancelsize += trashamounts[0];
		cancelsize += trashamounts[1]*2;
		cancelsize += trashamounts[2]*3;
		for(int i = [trashiconqueue_ count] - 1; i >= 0; i--) {
			if(cancelsize <= 0) break;
			TrashIcon* icon = [trashiconqueue_ objectAtIndex:i];
			Block* block = icon->block_;
			if(block) {
				cancelsize -= [block ySize];
				[self cancelTrashForIcon:icon];
			}
		}
#endif // #if CANCELTRASH

		// Dump trash on opponent
		[opponent_ receiveTrash:trashamounts chaingroup:chaingroup];

		// Update trashamounts for burst at the end of chain
		NSNumber* groupnum = [NSNumber numberWithInt:chaingroup];
		ChaingroupStats* stats = [chaingroupstats_ objectForKey:groupnum];
		if(stats != nil) {
			stats->trashamounts_[0] += trashamounts[0];
			stats->trashamounts_[1] += trashamounts[1];
			stats->trashamounts_[2] += trashamounts[2];
			CGPoint mid = ccp(([b1 position].x + [b2 position].x) / 2,
					             ([b1 position].y + [b2 position].y) / 2);
			stats->lastmeltpt_ = mid;
			[chaingroupstats_ setObject:stats forKey:groupnum];
		}

#if TRASHBURST
		[self trashBurst:midpos trashamounts:trashamounts iscombo:iscombo];
#endif
	} else {
		int vacationpoints = trashamounts[1] * 2 + trashamounts[2] * 3;
		[self addVacationTime:vacationpoints];
	}
}


- (void) addBlocksAtRaiseLevel:(int)level {
	Block* b2;
	Block* newblocks[GRID_XSIZE];
	[common_ getBlocksAtRaiseLevel:level blockarray:newblocks];
	for(int i = 0; i < GRID_XSIZE; i++) {
		b2 = newblocks[i];
		if(b2) {
			CGRect rect = [b2 gridRect];
			Block* newblock = [Block blockInGrid:g_
											 pos:ccp(CGRectGetMinX(rect),CGRectGetMinY(rect)-raiselevel_)
				                             color:b2.col shape:b2.shape];
			[blocks_ addObject:newblock];
			[blocksdisplay_ addChild:newblock z:BLOCK_Z];
			if(heartsbeating_) [newblock animate];
			[newblock glow];
			[newblock updateDisplay];
		}
	}

	[self addGoalLine:level];
}

- (void) addGoalLine:(int)level {
	int ypos = -PX_YSIZE;
	if(level == 0) {
		ypos = -BLOCK_YSIZE;
	}
	level -= selecteddepth_; // hack in selected depth for display purposes
	// Add goalline
	if(level != 0 && (-level) % GOALLINE_INTERVAL == 0) {
		goalline_[nextline_] = [[[TopClippedSprite alloc] initInGrid:g_] autorelease];
		[goalline_[nextline_] setTextureRect:CGRectMake(0, 0, PX_XSIZE, 2)];
		[goalline_[nextline_] setOpacity: 0xA0];
		[blocksdisplay_ addChild:goalline_[nextline_] z:GOALLINE_Z];
		goalline_[nextline_].position = ccp(PX_XSIZE/2, ypos);

		CCTexture2D* labeltex = [[[CCTexture2D alloc] initWithString:[NSString stringWithFormat:@"%d", -level]
			fontName:@"BPreplay" fontSize:20] autorelease];
		goallabel_[nextline_] = [[[TopClippedSprite alloc] initInGrid:g_] autorelease];
		[goallabel_[nextline_] setTexture:labeltex];
		CGSize size = [labeltex contentSize];
		[goallabel_[nextline_] setTextureRect: CGRectMake(0, 0, size.width, size.height)];
		[goalline_[nextline_] addChild:goallabel_[nextline_]];
		goallabel_[nextline_].anchorPoint = ccp(0, 0);
		goallabel_[nextline_].position = ccp(5, 5);
		if(nextline_ == 0) nextline_ = 1; else nextline_ = 0;
	}
}

- (void) raiseField:(ccTime) risedelay {
	// Raise goalline if any
	for(int i = 0; i < 2; i++) {
		if(goalline_[i]) {
			CGPoint pos = goalline_[i].position;
			pos.y += BLOCK_YSIZE;
			goalline_[i].position = pos;
			if(pos.y > PX_YSIZE) {
				[blocksdisplay_ removeChild:goallabel_[i] cleanup:true];
				[blocksdisplay_ removeChild:goalline_[i] cleanup:true];
				goalline_[i] = goallabel_[i] = nil;
			}
		}
	}

	// Add new blocks to the bottom of the field from the common generated stack.
	[self addBlocksAtRaiseLevel:raiselevel_-GRID_YSIZE];

	// raise all blocks, including those falling and melting
	for(Block* b2 in blocks_) {
		CGPoint gridpos = b2.gridpos;
		gridpos.y += 1;
		[b2 setGridpos: gridpos];
	}

	raiselevel_--;
	[self syncGrid];
	[self checkUnsupported:0 chaingroup:-1];

	// Add more blocks to the common generated stack if we ran out
	if(raiselevel_-GRID_YSIZE == -common_->treedepth_) {
		[common_ generateRandomTreeWithDepth:GENTREE_HEIGHT startheight:raiselevel_-GRID_YSIZE];
	}

	if(secondtouchactive_) {
		// deal with multi-touch raise being held
		distancescrolledpx_ = MAX(0, distancescrolledpx_-BLOCK_YSIZE);
		CGPoint pos1 = starttouchpx_, pos2 = secondtouchstartpx_;
		pos1.y = MIN(currenttouchpx_.y, pos1.y + BLOCK_YSIZE);
		pos2.y = MIN(secondtouchcurrentpx_.y, pos2.y + BLOCK_YSIZE);
		starttouchpx_ = pos1;  secondtouchstartpx_ = pos2;
		CGPoint pos = blocksdisplay_.position;
		pos.y = distancescrolledpx_ + PX_BOTTOM;
		[blocksdisplay_ setPosition: pos];
	} else {
		if(starttouchpx_.y + BLOCK_YSIZE < PX_YSIZE) {
			// move cursor/previewblock position along with the stack
			starttouchpx_.y += BLOCK_YSIZE;
			CGPoint cursorpos = cursor_.position;
			cursorpos.y += BLOCK_YSIZE;
			cursor_.position = cursorpos;
			[self placePreview];
		}

		// use this offset animation to fake the appearance of a gradual rise
		CGPoint pos = blocksdisplay_.position;
		pos.y = PX_BOTTOM-BLOCK_YSIZE;
		blocksdisplay_.position = pos;
		[blocksdisplay_ runAction:[CCSequence actions:
									   [CCMoveTo actionWithDuration:risedelay position:ccp(PX_LEFT,PX_BOTTOM)],
															   nil]];
	}

	// Tutorial block may need to be repositioned
	if(tutorialblock_) {
		[self removeTutorialBlock];
		[self placeTutorialBlock];
	}
}

- (ccTime) raiseTime:(int)depth {
#if INFINITEMODE
	return 0.5;
#endif // #if INFINITEMODE
	if(opponent_) { // VS player/AI raise speed relative to game time
		switch(g_->difficulty) {
			case EASY:
				return 300.0 / (gamecounter_ + 30.0);
			case MEDIUM:
#if STEELBOT6
				return 200.0 / (gamecounter_ + 90.0);
#else
				return 200.0 / (gamecounter_ + 30.0);
#endif
			case HARD:
				return 120.0 / (gamecounter_ + 30.0);
			default:
				assert(false);
				return 0;
		}
	} else {
		if(g_->practice) {
			return 5;
		}

		// Single player mode relative to depth in stack
		int stratum = (depth / 100);
		int subdepth  = depth % 100;

		int add = 30 + (30 * stratum);

		ccTime ret = 200.0 / (subdepth + add);

		// Extra slack for very new players who haven't learned the place-block gesture
		if(placehint_ && heartsrecord_ <= 5) {
			ret *= 3;
		}

		return ret;
	}
}

- (void) addVacationTime:(int)points {
	if(points == 0) return;

	for(int i = 0; i < points; i++) vacationtime_ += 0.25 / (vacationtime_ + 1);
	[self setVacationString];

	vacationtimelabel_.scaleX = 2;
	vacationtimelabel_.scaleY = 2;
	[vacationtimelabel_ runAction:[CCSequence actions:
		[CCScaleTo actionWithDuration:TRASHMOVETIME scaleX:1.0 scaleY:1.0],
			nil]];
}

-(void) killTrashIcon:(id)sender {
	[trashiconqueue_ removeObject:sender];
	[trashicondisplay_ removeChild:sender cleanup:true];
	[self rearrangeTrashIcons];
}

-(void) endHatJump:(id)sender {
	[mascotheads_[g_->mascot - 1] setTexture:[[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char%d-v2/char%d-head-selected.png", g_->mascot, g_->mascot]]];
	hat_.visible = false;
}

- (void) placePreview {
	if(!touchactive_) return;
	int prevshape = previewblock_.shape;
	CGPoint prevpos = previewblock_.gridpos;

	// Figure out the orientation of the preview block based on the relative position of the touches,
	// the type of block and whether the field is displayed upside down.
	float xdist = fabs(currenttouchpx_.x - starttouchpx_.x);
	float ydist = fabs(currenttouchpx_.y - starttouchpx_.y);
	if(currenttouchpx_.x <= starttouchpx_.x && xdist >= ydist) { currentshape_ = RIGHT; } else
	if(currenttouchpx_.x >= starttouchpx_.x && xdist >= ydist) { currentshape_ = LEFT; } else
	if(currenttouchpx_.y <= starttouchpx_.y && xdist <= ydist) { currentshape_ = UP; } else
	if(currenttouchpx_.y >= starttouchpx_.y && xdist <= ydist) { currentshape_ = DOWN; }
	spawnpointgrid_ = [self pxToGrid:starttouchpx_];
	if(currentshape_ == RIGHT) {
		spawnpointgrid_.x -= 2;
	} else if(currentshape_ == UP) {
		spawnpointgrid_.y -= 2;
	}
	BlockShape oldshape = currentshape_;
	if([common_ getBlockDoublenessToPlace:blocksplaced_]) {
		if(currentshape_ == RIGHT || currentshape_ == LEFT) currentshape_ = LEFTRIGHT;
		if(currentshape_ == UP || currentshape_ == DOWN) currentshape_ = UPDOWN;
	}
	[previewblock_ setShape:currentshape_];
	[previewblock_ setGridpos:spawnpointgrid_];

	[self checkPreviewValid];

	// Try readjusting by one as a player aid
	if(!previewblock_.visible) {
		switch(oldshape) {
			case LEFT:
				spawnpointgrid_.x -= 1;
				break;
			case RIGHT:
				spawnpointgrid_.x += 1;
				break;
			case UP:
				spawnpointgrid_.y += 1;
				break;
			case DOWN:
				spawnpointgrid_.y -= 1;
				break;
			default:
				break;
		}
		[previewblock_ setGridpos:spawnpointgrid_];
		[self checkPreviewValid];
	}
#if FLEXCURSOR
	if(!previewblock_.visible) {
		switch(oldshape) {
			case LEFT:
				spawnpointgrid_.x += 2;
				break;
			case RIGHT:
				spawnpointgrid_.x -= 2;
				break;
			case UP:
				spawnpointgrid_.y -= 2;
				break;
			case DOWN:
				spawnpointgrid_.y += 2;
				break;
			default:
				break;
		}
		[previewblock_ setGridpos:spawnpointgrid_];
		[self checkPreviewValid];
	}
#endif

	if(previewvalid_ && prevshape != currentshape_ && (prevpos.x != spawnpointgrid_.x || prevpos.y != spawnpointgrid_.y)) {
		[scene playSound:@"Assets/Sounds/click.caf" volume:1.0];
	}

	if(previewvalid_ || cursorvalid_) {
		[self crouch];
	} else {
		[self uncrouch];
	}
}

- (void) crouch {
	if(crouched_) return;
	crouched_ = true;

	[mascot_ stopAllActions];
	[fingers_ stopAllActions];
	[shadow_ stopAllActions];
	[robotLEDbox_ stopAllActions];
	[robotlogo_ stopAllActions];
	[mascot_ runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0], [SetTextureAction actionWithTexture:mascottex_[1]], nil]];
	[fingers_ runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0], [SetTextureAction actionWithTexture:fingerstex_[1]], nil]];
	[shadow_ runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0], 
		[CCScaleTo actionWithDuration:0 scaleX:SHADOWSCALE scaleY:SHADOWSCALE],
		nil]];
	if(robotLEDbox_) {
		[robotLEDbox_ runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0],
			[CCPlace actionWithPosition: robotLEDPos(1, g_->mirrored)],
			[CCRotateTo actionWithDuration:0 angle:robotLEDAngleDegrees(1, g_->mirrored)],
			nil]];
	}
	[currentblock_ stopActionByTag:1];
	CCAction* move = [CCMoveTo actionWithDuration:0.05 position:CROUCHPOS];
	move.tag = 1;
	[currentblock_ runAction:move];
}

- (void) uncrouch {
	if(!crouched_) return;
	[currentblock_ stopActionByTag:1];
	CCAction* move = [CCMoveTo actionWithDuration:0.1 position:CURPOS];
	move.tag = 1;
	[currentblock_ runAction:move];
	[mascot_ stopAllActions];
	[fingers_ stopAllActions];
	[shadow_ stopAllActions];
	[robotLEDbox_ stopAllActions];
	[mascot_ runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0.05],
		[SetTextureAction actionWithTexture:mascottex_[0]], nil]];
	[fingers_ runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0.05],
		[SetTextureAction actionWithTexture:fingerstex_[0]], nil]];
	[shadow_ runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0.05], 
		[CCScaleTo actionWithDuration:0 scaleX:SHADOWSCALE scaleY:SHADOWSCALE],
		nil]];
	[robotLEDbox_ runAction:[CCSequence actions:[CCDelayTime actionWithDuration:0.05],
		[CCPlace actionWithPosition: robotLEDPos(0, g_->mirrored)], 
		[CCRotateTo actionWithDuration:0 angle:robotLEDAngleDegrees(0, g_->mirrored)],
		nil]];
	crouched_ = false;
}

- (void) checkPreviewValid {
	if(!touchactive_) { return; }

	if(secondtoucheveractive_) {
		previewvalid_ = false; cursorvalid_ = false;
		cursor_.visible = previewblock_.visible = false;
		return;
	}

	CGRect gridRect = [previewblock_ gridRect];

	CGPoint startpointgrid = [self pxToGrid:starttouchpx_];
	CGPoint endpointgrid = [self pxToGrid:currenttouchpx_];
	
	// Check against grid boundaries
	previewvalid_ = true;
	if(startpointgrid.x == endpointgrid.x && startpointgrid.y == endpointgrid.y) {
		previewvalid_ = false;
	} else if(CGRectGetMaxX(gridRect) > GRID_XSIZE || CGRectGetMaxY(gridRect) > GRID_YSIZE ||
			  CGRectGetMinX(gridRect) < 0 || CGRectGetMinY(gridRect) < 0) {
		previewvalid_ = false;
	}

	CGRect cursorRect;
	cursorRect.origin.x = startpointgrid.x;
	cursorRect.origin.y = startpointgrid.y;
	cursorRect.size.width = 1;
	cursorRect.size.height = 1;
	cursorvalid_ = true;
#if !FLEXCURSOR
	if(CGRectGetMaxX(cursorRect) > GRID_XSIZE || CGRectGetMaxY(cursorRect) > GRID_YSIZE ||
			  CGRectGetMinX(cursorRect) < 0 || CGRectGetMinY(cursorRect) < 0) {
		cursorvalid_ = false;
	}

	CGRect altCursorRect = cursorRect;
	altCursorRect.origin.y += 1;
	bool altcursorvalid = true;
#endif

	// Check against blocks
	for(Block* b2 in blocks_) {
		if(b2.ismelting) continue;
		if(CGRectIntersectsRect(gridRect, [b2 gridRect])) {
			previewvalid_ = false;
		}
		if(CGRectIntersectsRect(cursorRect, [b2 gridRect])) {
			cursorvalid_ = false;
		}
#if !FLEXCURSOR
		if(CGRectIntersectsRect(altCursorRect, [b2 gridRect])) {
			altcursorvalid = false;
		}
#endif
	}

#if !FLEXCURSOR
	// be more generous about allowing touch within block (for when stack just raised)
	if(!cursorvalid_ && altcursorvalid) {
		starttouchpx_.y += BLOCK_YSIZE;
		CGPoint cursorpos = cursor_.position;
		cursorpos.y += BLOCK_YSIZE;
		cursor_.position = cursorpos;
		cursorvalid_ = true;
	}
#endif
	
	cursor_.visible = (FLEXCURSOR || cursorvalid_) && !previewvalid_;
	previewblock_.visible = previewvalid_;
}

- (void) mascotSelectTouch:(UITouch *)touch newTouch:(bool)newtouch{
	// Mascot-head touch
	CGSize headsize = [mascotheads_[0] contentSize];
	CGRect headrect = CGRectMake(0,0,headsize.width,headsize.height);
	if(CGRectContainsPoint(headrect, [mascotheads_[0] convertTouchToNodeSpace:touch])) { if(newtouch || g_->mascot != KEITH) [self setMascot:KEITH byPlayer:true]; return; }
	if(CGRectContainsPoint(headrect, [mascotheads_[1] convertTouchToNodeSpace:touch])) { if(newtouch || g_->mascot != BEAR) [self setMascot:BEAR byPlayer:true]; return; }
	if(mascotheads_[2].visible && CGRectContainsPoint(headrect, [mascotheads_[2] convertTouchToNodeSpace:touch])) { if(newtouch || g_->mascot != ROBOT) [self setMascot: ROBOT byPlayer:true]; return; }

	if(showdifficulty_) {
	// difficulty select touch
		CGSize diffsize = [difficulties_[0] contentSize];
		CGRect diffrect = CGRectMake(0,0,diffsize.width,diffsize.height);
		if(CGRectContainsPoint(diffrect, [difficulties_[0] convertTouchToNodeSpace:touch])) { [self setDifficulty: EASY byPlayer:true]; return; }
		if(CGRectContainsPoint(diffrect, [difficulties_[1] convertTouchToNodeSpace:touch])) { [self setDifficulty: MEDIUM byPlayer:true]; return; }
		if(CGRectContainsPoint(diffrect, [difficulties_[2] convertTouchToNodeSpace:touch])) { [self setDifficulty: HARD byPlayer:true]; return; }
	}

	// Speed select arrows
	if(scene.layout_ == SINGLEPLAYER && speedlabel_) {
		if(isTouchInSprite(touch, speedarrowleft_)) {
			[self moveSpeedLines:true];
			return;
		}
		if(isTouchInSprite(touch, speedarrowright_)) {
			[self moveSpeedLines:false];
			return;
		}
	}

	// Ready button touch
	CGSize readysize = [readybutton_ contentSize];
	CGRect readyrect = CGRectMake(0,0,readysize.width,readysize.height);
	if(CGRectContainsPoint(readyrect, [readybutton_ convertTouchToNodeSpace:touch])) {
		g_->is_ready = true;
		CGPoint oldpos = readybutton_.position;
		[self removeChild:readybutton_ cleanup:true];
		readybutton_ = [CCLabel labelWithString:[NSString stringWithFormat:@"Waiting for Player %d", (g_->playerno ? 1 : 2)] fontName:@"BPreplay" fontSize:30];
		[readybutton_ setColor:ccc3(0x0,0x0,0)];
		readybutton_.position = oldpos;
		[self addChild:readybutton_ z:WINLOSE_Z];
		waitinghighlight_.visible = true;
		[self startRaiseIfReady];
		[opponent_ startRaiseIfReady];
		[scene playSound:@"Assets/Sounds/chain01.caf" volume:1.0];

		return;
	}
}

- (BOOL) checkStatsButtons:(UITouch*) touch {
	if(scene.layout_ == SINGLEPLAYER && scene.state_ == ENDED_SOLO && statsview_) {
		if(isTouchInSprite(touch, restartbutton_)) {
			g_->is_ready = false; // force mascot select
			[scene playMusic:@"Assets/Music/Fralke.m4a"];
			[scene changeLayout:scene.layout_];
			[scene changeState:PLAYING];
			return true;
		}
		if(isTouchInSprite(touch, endmenubutton_)) {
			[scene changeState:MAINMENU];
			return true;
		}
	}
	return false;
}

- (BOOL) checkSideButtons:(UITouch*) touch {
	// First check for hits on the side buttons
	if(g_->playerno == 0 || scene.layout_ == TWOPLAYERSREVERSED) {
		if(isTouchInSprite(touch, menubutton_)) {
			[scene beginPause:true isRotated:(g_->playerno == 1)];
			return true;
		}
		if(isTouchInSprite(touch, pausebutton_)) {
			[scene beginPause:false isRotated:(g_->playerno == 1)];
			return true;
		}
		if((ingamestate_ == PLAY || ingamestate_ == MASCOTSELECT) && (scene.state_ == PLAYING || scene.state_ == ENDED_SOLO) && isTouchInSprite(touch, helpbutton_)) {
			[scene changeState:INSTRUCTIONS];
			return true;
		}
	}
	return false;
}

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
	if(scene.state_ == INSTRUCTIONS) return false;
	if([self checkSideButtons:touch]) {
		return true;
	}
	if(g_->is_ai) return false;

	// Get board-relative touch coords
	CGPoint location = [blocksdisplaybase_ convertTouchToNodeSpace:touch];
	if(location.x < -150 || location.x >= PX_XSIZE + 150) return false;
	CGPoint gridpt = [self pxToGrid:location];
	
#if !FLEXCURSOR
	if(gridpt.x >= GRID_XSIZE || gridpt.x < 0 || gridpt.y >= GRID_YSIZE || gridpt.y < 0) return false;
#else
	if(gridpt.x >= GRID_XSIZE + 1 || gridpt.x < -1 || gridpt.y >= GRID_YSIZE + 1 || gridpt.y < -1) return false;
#endif

	// Check for mascot and difficulty selections
	if(ingamestate_ == MASCOTSELECT) {
		[self mascotSelectTouch:touch newTouch:true];
		return true;
	}

	if(ingamestate_ != PLAY) return false;

	// Handle multitouch for stack raising
	if(touchactive_) {
		if(secondtouchactive_) return false;
		secondtoucheveractive_ = secondtouchactive_ = true;
		secondtouch = touch;
		secondtouchcurrentpx_ = secondtouchstartpx_ = location;
		starttouchpx_ = currenttouchpx_; // hack to avoid jump if first finger already moved

		// terminate ongoing raise animation if any
		[blocksdisplay_ stopAllActions];
		blocksdisplay_.position = ccp(PX_LEFT, PX_BOTTOM);

		for(int i = 0; i < 3; i++) scrollarrows_[i].visible = true;
		return true;
	}
	currenttouchpx_ = starttouchpx_ = location;
	secondtoucheveractive_ = secondtouchactive_ = false;
	firsttouch = touch;

	// Place cursor/previewblock
	cursor_.position = ccp((gridpt.x + 0.5) * BLOCK_XSIZE, (gridpt.y + 0.5) * BLOCK_YSIZE);
	cursor_.scale = SCALEFACTOR;
	[cursor_ setColor:drawColor([common_ getBlockColorToPlace:blocksplaced_], g_->mascot, PLACE_CURSOR)];
	cursor_.visible = true;
	[previewblock_ setCol:[common_ getBlockColorToPlace:blocksplaced_]];
	[previewblock_ setShape:[common_ getBlockDoublenessToPlace:blocksplaced_] ? LEFTRIGHT : LEFT];
	[previewblock_ setOpacity:0x60];
	previewblock_.visible = false;
	touchactive_ = true;

	[self placePreview];

	return true;
}

- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
	if(ingamestate_ == MASCOTSELECT) {
		[self mascotSelectTouch:touch newTouch:false];
	}

	if(ingamestate_ != PLAY) return;

	CGPoint location = [blocksdisplaybase_ convertTouchToNodeSpace:touch];
	if(touch == firsttouch) {
		currenttouchpx_ = location;
		[self placePreview];
	}

	if(secondtouchactive_) {
		if(touch == secondtouch) {
			secondtouchcurrentpx_ = location;
		}
		int olddist = distancescrolledpx_;
		
		// Two-finger scroll movement.  Average the distance of each finger
		distancescrolledpx_ = (MAX(0, currenttouchpx_.y - starttouchpx_.y)
							 + MAX(0, secondtouchcurrentpx_.y - secondtouchstartpx_.y)) / 2;
		if(!INFINITEMODE && [self isDying:fabs(distancescrolledpx_ / BLOCK_YSIZE)]) {
			distancescrolledpx_ = olddist;
			return;
		}
		CGPoint pos = blocksdisplay_.position;
		pos.y = distancescrolledpx_ + PX_BOTTOM;
		blocksdisplay_.position = pos;
	}
}

- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
	if(ingamestate_ != PLAY) return;

	CGPoint location = [blocksdisplaybase_ convertTouchToNodeSpace:touch];
	if(touch == firsttouch) {
		currenttouchpx_ = location;
		[self endTouch];
	}
	if(touch == secondtouch && secondtouchactive_) {
		[self snapStackToTouch];
		secondtouchactive_ = false;
	}
}

- (void)ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event {
	[self ccTouchEnded:touch withEvent:event];
}

- (void)endTouch {
	if(!touchactive_) return;

	[self checkPreviewValid];

	cursor_.visible = false;
	previewblock_.visible = false;
	touchactive_ = false;
	if(secondtouchactive_) {
		[self snapStackToTouch];
		secondtouchactive_ = false;
	}

	if(previewvalid_) {
		[self throwBlock];
	} else {
		[self uncrouch];
	}
}

- (void) snapStackToTouch {
	int numlinesraised = floor(fabs((float) distancescrolledpx_) / BLOCK_YSIZE);

	for(int i = 0; i < numlinesraised; i++) {
		[self raiseField:RISEDELAY];
	}

	if(numlinesraised > 0) {
	   	raisecounter_ = 0.0f;
		numlinesmanuallyraised_ += numlinesraised; 
		int old = [[NSUserDefaults standardUserDefaults] integerForKey:D_LINESMANUALLYRAISED];
		[[NSUserDefaults standardUserDefaults] setInteger:old+numlinesraised forKey:D_LINESMANUALLYRAISED];
	}

	blocksdisplay_.position = ccp(PX_LEFT, PX_BOTTOM);

	for(int i = 0; i < 3; i++) scrollarrows_[i].visible = false;
}

- (CGPoint) pxToGrid:(CGPoint)relPt {
	CGPoint gridPt = ccp(floor(relPt.x / BLOCK_XSIZE), floor(relPt.y / BLOCK_YSIZE));
	return gridPt;
}


- (void)throwBlock {
	Block* block = [Block blockInGrid:g_ pos:spawnpointgrid_
									   color:[common_ getBlockColorToPlace:blocksplaced_]
									   shape:currentshape_];

	// interrupt melting of blocks we're throwing on
	CGRect gridRect = [block gridRect];
RESTART:
	for(Block* b2 in blocks_) {
		if(CGRectIntersectsRect(gridRect, [b2 gridRect]) && b2.ismelting) {
			[self endMelt:b2];
			goto RESTART;
		}
	}

	[blocks_ addObject:block];
	[blocksdisplay_ addChild:block z:BLOCK_Z];
	[block startFalling:0 chaingroup:-1 delay:SPAWNDELAY];

	CGPoint jumpstart = [blocksdisplay_ convertToNodeSpace:[mascot_ convertToWorldSpace:currentblock_.position]];
	float rotation = block.rotation;
	CGRect pxrect = [block pxRect];
	CGPoint jumpend = ccp(CGRectGetMidX(pxrect), CGRectGetMidY(pxrect));
	// aim one block higher if the stack is going to raise by the time it lands
	if(raisecounter_ + SPAWNDELAY >= [self raiseTime:MAX(0, -highestblocklevel_)]) {
		jumpend.y += BLOCK_YSIZE;
	}

	block.position = jumpstart;
	block.rotation = 0;
	block.isbeingthrown = true;
	[block runAction:[CCJumpTo actionWithDuration:SPAWNDELAY position:jumpend height:100 jumps:1]];
	[block runAction:[CCRotateTo actionWithDuration:SPAWNDELAY angle:rotation]];
	[block runAction:[CCSequence actions:[CCDelayTime actionWithDuration:SPAWNDELAY],
								   [CCCallFuncN actionWithTarget:self selector:@selector(endPlace:)],
														   nil]];
	if(heartsbeating_) [block animate];
	[block glow];

	[mascot_ stopAllActions];
	[fingers_ stopAllActions];
	[shadow_ stopAllActions];
	[robotLEDbox_ stopAllActions];
	[mascot_ runAction:[CCSequence actions:
			[SetTextureAction actionWithTexture:mascottex_[2]],
			[CCDelayTime actionWithDuration:0.1],
			[SetTextureAction actionWithTexture:mascottex_[3]],
			[CCDelayTime actionWithDuration:0.2],
			[SetTextureAction actionWithTexture:mascottex_[0]],
			nil]];
	[fingers_ runAction:[CCSequence actions:
			[SetTextureAction actionWithTexture:emptytex],
//			[SetTextureAction actionWithTexture:fingerstex_[2]],
			[CCDelayTime actionWithDuration:0.1],
//			[SetTextureAction actionWithTexture:fingerstex_[3]],
			[CCDelayTime actionWithDuration:0.2],
			[SetTextureAction actionWithTexture:fingerstex_[0]],
			nil]];

	[shadow_ runAction:[CCSequence actions:
			[CCScaleTo actionWithDuration:0 scaleX:SHADOWSCALE*0.93 scaleY:SHADOWSCALE*0.93],
			[CCDelayTime actionWithDuration:0.1],
			[CCScaleTo actionWithDuration:0 scaleX:SHADOWSCALE*0.85 scaleY:SHADOWSCALE*0.85],
			[CCDelayTime actionWithDuration:0.2],
			[CCScaleTo actionWithDuration:0 scaleX:SHADOWSCALE scaleY:SHADOWSCALE],
			nil]];
	if(robotLEDbox_) {
		[robotLEDbox_ runAction:[CCSequence actions:
			[CCPlace actionWithPosition:robotLEDPos(2, g_->mirrored)],
			[CCRotateTo actionWithDuration:0 angle:robotLEDAngleDegrees(2, g_->mirrored)],
			[CCDelayTime actionWithDuration:0.1],
			[CCPlace actionWithPosition:robotLEDPos(3, g_->mirrored)],
			[CCRotateTo actionWithDuration:0 angle:robotLEDAngleDegrees(3, g_->mirrored)],
			[CCDelayTime actionWithDuration:0.2],
			[CCPlace actionWithPosition:robotLEDPos(0, g_->mirrored)],
			[CCRotateTo actionWithDuration:0 angle:robotLEDAngleDegrees(0, g_->mirrored)],
			nil]];
	}


	crouched_ = false;
	hintcounter_ = 0;
	if(placehint_) {
		placehint_ = false;
		[self speechBubble:nil];
	}

	blocksplaced_++;
	[self rotateBlockColor];

	[self removeTutorialBlock];
}

- (void) endPlace:(id)block {
	[block setIsbeingthrown: false];
	[block updateDisplay];
}

- (void) rotateBlockColor {
	currentblock_.col = [common_ getBlockColorToPlace:blocksplaced_];
	nextblock_.col =  [common_ getBlockColorToPlace:blocksplaced_+1];
	currentblock_.visible = nextblock_.visible = true;
	[currentblock_ setShape:![common_ getBlockDoublenessToPlace:blocksplaced_] ? LEFT : LEFTRIGHT];
	[nextblock_ setShape:![common_ getBlockDoublenessToPlace:blocksplaced_+1] ? LEFT : LEFTRIGHT];

	int opacity = 0x40;
	nextblock_.position = NEXTPOS;
	[nextblock_ setOpacity:opacity];
	[nextblock_->heart1 setOpacity:opacity];
	[nextblock_->heart2 setOpacity:opacity];

	{
		currentblock_.position = NEXTPOS;
		nextblock_.position = NEXTPOS;
		[currentblock_ stopActionByTag:1];
		[currentblock_ setOpacity:opacity];
		[currentblock_->heart1 setOpacity:opacity];
		[currentblock_->heart2 setOpacity:opacity];
		CCAction* move = [CCSequence actions:
			[CCDelayTime actionWithDuration:0.3],
			[CCMoveTo actionWithDuration:0.2 position:CURPOS],
			nil];
		move.tag = 1;
		[currentblock_ runAction:move];
		[currentblock_ runAction:[CCFadeTo actionWithDuration:0.3 opacity:0xFF]];
		[currentblock_->heart1 runAction:[CCFadeTo actionWithDuration:0.3 opacity:0xFF]];
		[currentblock_->heart2 runAction:[CCFadeTo actionWithDuration:0.3 opacity:0xFF]];

		nextblock_.visible = false;
		[nextblock_ runAction:[CCSequence actions: [CCDelayTime actionWithDuration:0.3], [CCShow action], nil]];
	}

	[self updateBackgroundSlices];
}

- (void) updateBackgroundSlices {
	BlockColor col = currentblock_.col;
	if(g_->is_ai) col = BLUE;
	// Fade in background color to color of new block
	ccColor3B bright = drawColor(col, g_->mascot, BG_BRIGHT),
			  dim = drawColor(col, g_->mascot, BG_DIM);
	bool dark = false;
	for(int i = 0; i < 8; i++) {
		ccColor3B c;
		if(dark) {
			c = dim;
		} else {
			c = bright;
		}
		dark = !dark;
		[slices_[i] runAction:[CCSequence actions: 
							[CCTintTo actionWithDuration:0.3 red:c.r green:c.g blue:c.b],
							nil]];
	}
}

- (void) receiveTrash:(int[3])amounts chaingroup:(int)chaingroup {
	int i, j;
	int numadded = 0;

	CGPoint iconend = ccp(0,ICONYOFFSET);

	ccTime delay = 0.0;

	TrashIcon* icon;

	// Add each of the kinds of trash to the icon queue.
	for(i = 0; i < 3; i++) {
		BlockShape shape = (BlockShape) (TRASHx1 << i);
		for(j = 0; j < amounts[i]; j++) {
			Block* trash = [Block blockInGrid:g_ pos:ccp(-1,-1) color:TRASH shape:shape];
			[trashqueue_ addObject:trash];

		    icon = [[[TrashIcon alloc] initWithBlock:trash chaingroup:chaingroup] autorelease];
			[trashiconqueue_ addObject:icon];
			[trashicondisplay_ addChild:icon z:TRASHICONDISPLAY_Z];

			numadded++;

			icon.scaleX = 2;
			icon.scaleY = 2;

			[icon setPosition:iconend];
			[icon setOpacity:0];
			[icon runAction:[CCSequence actions:
				[CCDelayTime actionWithDuration:delay],
				[CCFadeTo actionWithDuration:0.2 opacity:0xFF],
				nil]];
			[icon runAction:[CCSequence actions:
				[CCDelayTime actionWithDuration:delay],
				[CCScaleTo actionWithDuration:TRASHMOVETIME scaleX:1.0 scaleY:1.0],
				nil]];
			delay += TRASHEMITDELAY;
			iconend.x += ICONXOFFSET;
		}
	}

	if(numadded == 0) return;

	// Show most recently added icons to the left (pushing other icons rightward)
	[self rearrangeTrashIcons];
}


- (void) rearrangeTrashIcons {
	int xoffset = 0;
	for(int i = [trashiconqueue_ count] - 1; i >= 0; i--) {
		TrashIcon* icon = [trashiconqueue_ objectAtIndex:i];
		// Terminate any existing horizontal move action (but not other kinds
		// of animations).
		[icon stopActionByTag:1];
		CGPoint pos = icon.position;
		if(pos.x != xoffset) {
			CCAction* a = [CCMoveTo actionWithDuration:TRASHMOVETIME position:ccp(xoffset, ICONYOFFSET)];
			a.tag = 1;
			[icon runAction:a];
		}
		xoffset += ICONXOFFSET;
	}
}

- (void) computeChainStats {
	NSArray* keys = [chaingroupstats_ allKeys];
	NSNumber* key;

	for(int i = 0; i < (int) [keys count]; i++) {
		key = [keys objectAtIndex:i];
		int chaingroup = [key intValue];
		if([self hasBlockWithChaingroup:chaingroup]) continue;

		ChaingroupStats* stats = [chaingroupstats_ objectForKey:key];

		// Add to overall chain stats
		int subtractno = 0;
		bool chainfound = false;
		for(int i = MAX_CHAIN - 1; i >= 2; i--) {
			if(stats->numchains_[i] - subtractno <= 0) continue;

			chainfound = true;
			// Adjust for lower chains also counting higher chains.
			int realnum = stats->numchains_[i] - subtractno;
			numchains_[i] += realnum;

			if(!g_->is_ai) {
				int record = [[NSUserDefaults standardUserDefaults] integerForKey:D_CHAINCOUNT(i)];
				record += realnum;
				[[NSUserDefaults standardUserDefaults] setInteger:record forKey:D_CHAINCOUNT(i)];
			}

			subtractno = numchains_[i];
		}
		if(chainfound) { blockschained_ += stats->numblocks_; }

		/*
		// Trash icon burst
		[self trashBurst:stats->lastmeltpt_ trashamounts:stats->trashamounts_];
		*/

		[chaingroupstats_ removeObjectForKey:key];
	}
}

- (void) trashBurst:(CGPoint)pos trashamounts:(int[3])trashamounts iscombo:(bool)iscombo {
	int t[3];
	t[0] = trashamounts[0];
	t[1] = trashamounts[1];
	t[2] = trashamounts[2];

	int dist = iscombo ? 120 : 200;
	bool reversed = g_->mirrored;
	double delta = 0.436332313; // 25 degrees
	if(reversed) delta = -delta;
//	double startangle = M_PI/2;
	double startangle = 0.1 * myrandom(64);
	double angle = startangle;
	for(int i = 0; i < 3; i++) {
		while(t[i] > 0) {
			int xdist = dist * cos(angle);
			int ydist = dist * sin(angle);
			angle += delta;

			CGPoint startpos = pos;
			startpos = [blocksdisplay_ convertToWorldSpace:startpos];
			startpos = [blocksdisplaybase_ convertToNodeSpace:startpos];

			CGPoint endpos = ccp(startpos.x + xdist, startpos.y + ydist);

			if(endpos.x < 0 || endpos.x > PX_XSIZE) {
				continue;
			}

			CCSprite* circle = [CCSprite spriteWithFile:@"Assets/Graphics/combo.png"];
			[circle setOpacity:0x30];

			CCSprite* icon;
			switch(i) {
				case 0:
					icon = [CCSprite spriteWithFile:@"Assets/Graphics/trashicon-1.png"];
					break;
				case 1:
					icon = [CCSprite spriteWithFile:@"Assets/Graphics/trashicon-2.png"];
					break;
				case 2:
					icon = [CCSprite spriteWithFile:@"Assets/Graphics/trashicon-3.png"];
					break;
				default:
					icon = [CCSprite spriteWithFile:@"Assets/Graphics/trashicon-1.png"];
					break;
			}
			circle.position = startpos;
			[blocksdisplaybase_ addChild:circle z:500];
			icon.position = ccp([circle contentSize].width / 2, [circle contentSize].height / 2);
			[circle addChild:icon z:2];
			icon.opacity = 0xFF;
			circle.scaleX = 0.7;
			circle.scaleY = 0.7;
			[icon runAction:[CCSequence actions:
	//			[CCDelayTime actionWithDuration:0.1],
					   [CCFadeTo actionWithDuration:0.6 opacity:0x0],
					   nil]];
			[circle runAction:[CCSequence actions:
					   [CCEaseOut actionWithAction:[CCMoveTo actionWithDuration:0.6 position:endpos] rate:2.0f],
					   nil]];
			[circle runAction:[CCSequence actions:
	//				[CCEaseOut actionWithAction:[CCScaleTo actionWithDuration:0.2 scaleX:0.7 scaleY:0.7] rate:5.0f],
				[CCFadeTo actionWithDuration:0.6 opacity:0x0],
			   [KillNodeAction action],
				nil]];

			t[i]--;
		}
	}
}

- (void) dropTrashFromQueue {
	if([trashqueue_ count] == 0)
		return;

	// Figure out the height of the stack on both the left 4 tiles and the right 4 tiles
	int stackleftheight = 0, stackrightheight = 0;
	for(Block *b in blocks_) {
		CGRect rect = [b gridRect];
		int height = CGRectGetMaxY(rect);
		if(CGRectGetMinX(rect) < GRID_XSIZE/2 && height > stackleftheight) { stackleftheight = height; }
		if(CGRectGetMaxX(rect) > GRID_XSIZE/2 && height > stackrightheight) { stackrightheight = height; }
	}

	// Same as stackleftheight, except at least the height of the board.  For initial positioning of a trash
	// block to drop from above.
	int dropleftheight = MAX(stackleftheight, GRID_YSIZE);
	int droprightheight = MAX(stackrightheight, GRID_YSIZE);

	Block* trash;
	for(int i = 0; i < (int) [trashqueue_ count]; i++) {
		trash = [trashqueue_ objectAtIndex:i];

		// Wait until opponent's chain is finished before dropping this icon.
		if([opponent_ hasBlockWithChaingroup:trash->trashicon->chaingroup_]) continue;

		bool dying_on_left = stackleftheight >= GRID_YSIZE;
		bool dying_on_right = stackrightheight >= GRID_YSIZE;

		// Prefer to drop trash on the side that's not already touching the top of the board.
		if(!(dying_on_left && dying_on_right)) {
			if(dying_on_left && nexttrashside_ == LEFT) { nexttrashside_ = RIGHT; }
			else if(dying_on_right && nexttrashside_ == RIGHT) { nexttrashside_ = LEFT; }
		}
		int dropx, dropy;
		if(nexttrashside_ == LEFT) {
			dropx = 0;
			dropy = dropleftheight;
		} else {
			dropx = GRID_XSIZE / 2;
			dropy = droprightheight;
		}

		// Transform the icon into real trash.
		[trash setGridpos:ccp(dropx, dropy)];
		[blocks_ addObject:trash];
		[trashqueue_ removeObjectAtIndex:i];
		i--;
		[blocksdisplay_ addChild:trash z:BLOCK_Z];
		[trash startFalling:0 chaingroup:-1 delay:0];
		
		// Keep track of the current height of the stack (projecting forward to
		// the point where the trash has already fallen on it), and of the height of the new trash blocks
		// we've just placed above where the player can see.
		if(nexttrashside_ == LEFT) {
			dropleftheight += [trash ySize];
			stackleftheight += [trash ySize];
			nexttrashside_ = RIGHT;
		} else {
			droprightheight += [trash ySize];
			stackrightheight += [trash ySize];
			nexttrashside_ = LEFT;
		}
	}
}

- (void) cancelTrashForIcon:(TrashIcon*)icon {
	if(icon == nil || icon->cancelled_) return;

	icon->cancelled_ = true;

	// Cancelling animation on icon
	icon->cancelslash_ = [CCSprite spriteWithFile:@"Assets/Graphics/cancel-trash.png"];
	[icon addChild:icon->cancelslash_ z:2];
	icon.opacity = 0xA0;
	[icon runAction:[CCSequence actions:
           	   [CCDelayTime actionWithDuration:1.0], 
			   [CCScaleTo actionWithDuration:0.3 scaleX:0.01 scaleY:0.01],
			   [CCCallFuncN actionWithTarget:self selector:@selector(killTrashIcon:)],
													   nil]];

	// Delete trash block itself
	if(icon->block_) {
		int index = [trashqueue_ indexOfObject:icon->block_];
		if(index != NSNotFound) {
			// Delete from pending queue if it's there
			[trashqueue_ removeObjectAtIndex:index];
		} else {
			// Remove from playing field if it's there (presumably above
			// where the player can see)
			[self endMelt: icon->block_];
		}
		icon->block_ = nil;
	}
}

- (void) dropTrashIconForBlock:(Block*)block {
	if(block == nil || block.col != TRASH || block->trashicon == nil || block->trashicon->cancelled_) return;
	[block->trashicon runAction:[CCSequence actions:
			   [CCMoveBy actionWithDuration:0.1 position:ccp(0, -ICONYOFFSET * 2)],
			   [CCCallFuncN actionWithTarget:self selector:@selector(killTrashIcon:)],
													   nil]];
	block->trashicon->block_ = nil;
	block->trashicon = nil;
}

- (bool) isDying:(float)offset {
	CGRect dierect;
	dierect.origin.x = 0;
	dierect.origin.y = GRID_YSIZE - 0.1 - offset;
	dierect.size.width = GRID_XSIZE;
	dierect.size.height = 0.1;

	bool ret = false;
	isdyingduetofalling_ = true;

	for(Block* b in blocks_) {
		if(CGRectIntersectsRect(dierect, [b gridRect])) {
			ret = true;
			if(!b.isfalling) { isdyingduetofalling_ = false; }
		}
	}

	return ret;
}

- (bool) predictDyingAfterStabilize {
	int floor[GRID_XSIZE];
	int i;
	for(i = 0; i < GRID_XSIZE; i++) { floor[i] = 0; }

	// Consider the height after current melts and falls are done.  Don't bother simulating chain melts since
	// we'd end up rewriting the whole game logic in an instantaneous form, and it's probably not necessary
	// to maintain a feeling of fairness.

	for(Block *b in blocks_) {
		// Fall clear through melting blocks
		if(b.ismelting) continue;

        if(b.gridpos.y <= 0) {
			// Blocks <= 0 height must be stabilized
			for(i = b.gridpos.x; i < b.gridpos.x + [b xSize]; i++) {
				floor[i] = MAX(floor[i], b.gridpos.y + [b ySize]);
			}
		} else {
			// Ignore y positions here, relying on assumption that blocks are sorted from bottom to top
			int landheight = 0;
			// Determine position after falling past melted blocks
			for(i = b.gridpos.x; i < b.gridpos.x + [b xSize]; i++) {
				landheight = MAX(floor[i], landheight);
			}
			// Adjust floor to top of stabilized position
			for(i = b.gridpos.x; i < b.gridpos.x + [b xSize]; i++) {
				floor[i] = landheight + [b ySize];
			}
		}
	}

	isdyingafterstabilize_ = false;
	for(i = 0; i < GRID_XSIZE; i++) { if(floor[i] >= GRID_YSIZE) isdyingafterstabilize_ = true; }
	return isdyingafterstabilize_;
}

- (bool) hasBlockWithChaingroup:(int)chaingroup {
	for(Block *b in blocks_) {
		if(b.chaingroup == chaingroup)
			return true;
	}

	return false;
}

- (bool) isReadyToStart {
	return g_->is_ready;
}


- (bool) isAi {
	return g_->is_ai;
}

- (void) setVacationString {
	int secs = floor(vacationtime_);
	int tenths = (vacationtime_ - secs) * 10;
	NSString* timestr = [NSString stringWithFormat:@"%d.%01d", secs, tenths];
	[vacationtimelabel_ setString:timestr];
}

- (void) setDepthString {
	int depth = MAX(0, -highestblocklevel_);
#if DEPTHLABEL
	NSString* depthstring = [NSString stringWithFormat:@"%d lines", depth];
	[depthlabel_ setString:depthstring];
#endif //#if DEPTHLABEL

	bool newrecord = false;
	if(scene.layout_ == SINGLEPLAYER && !g_->practice) {
		int maxdepth = [[NSUserDefaults standardUserDefaults] integerForKey:D_DEEPESTREACHED];
		if(depth > maxdepth) {
			[[NSUserDefaults standardUserDefaults] setInteger:depth forKey:D_DEEPESTREACHED];
			newrecord = true;
		}
	}

	int interval = LINESREACHED10 ? 10 : 100;
	if(depth > 0 && depth % interval == 0 && scene.layout_ == SINGLEPLAYER && !g_->practice) {
		[scene playSound:@"Assets/Sounds/trash.caf" volume:1.0];

		NSString* linesreachedstring = [NSString stringWithFormat:@"%d lines reached!", depth];
		[linesreachedlabel_ setString:linesreachedstring];

		linesreached_.position = g_->winstartpos;
		linesreached_.visible = true;
		newrecordlabel_.visible = false;
		if(newrecord) {
			newrecordlabel_.visible = true;
			newrecordlabel_.position = ccp(600, -100);
			[newrecordlabel_ runAction:[CCSequence actions:
				[CCDelayTime actionWithDuration:0.8], 
				[CCEaseOut actionWithAction:[CCMoveTo actionWithDuration:0.5 position:ccp(0, -100)] rate:3.0f],
				[CCCallFuncN actionWithTarget:self selector:@selector(clingSound:)],
				nil]];
		}

		[linesreached_ runAction:[CCSequence actions:
			[CCEaseOut actionWithAction:[CCMoveTo actionWithDuration:0.5 position:WINLABELPOS] rate:3.0f],
           	[CCDelayTime actionWithDuration:3.0], 
			[CCEaseIn actionWithAction:[CCMoveTo actionWithDuration:0.5 position:g_->winstartpos] rate:3.0f],
			nil]];
	}
}

- (void) alarmSoundCallback:(ccTime)dt {
	// Have a different backing siren for each player
	bool firstalarm = (scene.layout_ == SINGLEPLAYER) ? (g_->playerno == 0) : (g_->playerno == 1);
	if(firstalarm) {
		alarmsoundid_ = [scene playSound:@"Assets/Sounds/alarm-loop2.caf" volume:1.0];
	} else {
		alarmsoundid_ = [scene playSound:@"Assets/Sounds/alarm-loop.caf" volume:0.25];
	}
}

- (void) incrementDangerCounter:(ccTime)dt {
	if(!heartsbeating_) {
		for(Block* b in blocks_) {
			[b animate];
		}
		heartsbeating_ = true;
		[self alarmSoundCallback:0];
		[self schedule: @selector(alarmSoundCallback:) interval:1.0];
	}

	if(vacationtime_ > 0.0) {
		if(!isdyingafterstabilize_) return;
		vacationtime_ -= dt;
		if(vacationtime_ < 0.0) {
			dt = -vacationtime_;
			vacationtime_ = 0.0;
		}
		[self setVacationString];
		if([dangerglow_ numberOfRunningActions] == 0) { 
			[dangerglow_ runAction: [CCSequence actions:
				[CCFadeTo actionWithDuration:(DEATHCOUNTTIME/3) opacity:0x80],
				[CCFadeTo actionWithDuration:(DEATHCOUNTTIME/3) opacity:0],
				nil]];
		}
		if(vacationtime_ > 0.0) return;
	}

	dangercounter_ += dt;

	int newlabel = MAX(0, 3 - floor(dangercounter_ / DEATHCOUNTTIME));

	if(dangersign_) {
		if(deathnum_ <= newlabel) return;
	}

	[dangerglow_ stopAllActions];
	[dangerglow_ runAction: [CCSequence actions:
		[CCFadeTo actionWithDuration:(DEATHCOUNTTIME/3) opacity:0x40], // 0x80
		[CCFadeTo actionWithDuration:(DEATHCOUNTTIME/3) opacity:0],
		nil]];

	deathnum_ = newlabel;

	if(dangersign_) {
		[dangersign_ runAction: [CCSequence actions:
			[CCOrbitCamera actionWithDuration:CARDFLIPTIME radius:1 deltaRadius:0 angleZ:0 deltaAngleZ:85 angleX:0 deltaAngleX:0],
			[CCCallFuncN actionWithTarget:self selector:@selector(swapDangerSign:)],
								  nil]];
	} else {
		[self swapDangerSign:nil];
	}
}

- (void) swapDangerSign:(id)sender {
	CCTexture2D* tex = [[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/counter%d.png", deathnum_]];
	if(!dangersign_) {
		dangersign_ = [CCSprite spriteWithTexture:tex];
		dangersign_.position = DANGERSIGNPOS;
		if(scene.layout_ != SINGLEPLAYER) {
			dangersign_.scale = 0.7;
		}
		[[dangersign_ camera] setEyeX:1.0f eyeY:1.0f eyeZ:0.0f];
		[self addChild:dangersign_ z:DANGERSIGN_Z];
		vacationsquare_.visible = false;
		vacationlabel_.visible = false;
		vacationtimelabel_.visible = false;
		if(opponent_ == nil) { dangersign_.scaleX = dangersign_.scaleY = 1.0/MASCOTSCALE; }
	} else {
		[dangersign_ setTexture:tex];
	}

	bool firstalarm = (scene.layout_ == SINGLEPLAYER) ? (g_->playerno == 0) : (g_->playerno == 1);
	if(firstalarm) {
		switch(deathnum_) {
			case 3:
				[scene playSound:@"Assets/Sounds/count-lo.caf" volume:1.0];
				break;
			case 2:
				[scene playSound:@"Assets/Sounds/count-med.caf" volume:1.0];
				break;
			case 1:
				[scene playSound:@"Assets/Sounds/count-hi.caf" volume:1.0];
				break;
			default:
				break;
		}
	} else {
		switch(deathnum_) {
			case 3:
				[scene playSound:@"Assets/Sounds/count-lo-2.caf" volume:1.0];
				break;
			case 2:
				[scene playSound:@"Assets/Sounds/count-med-2.caf" volume:1.0];
				break;
			case 1:
				[scene playSound:@"Assets/Sounds/count-hi-2.caf" volume:1.0];
				break;
			default:
				break;
		}
	}

	[dangersign_ runAction: [CCSequence actions:
								[CCOrbitCamera actionWithDuration:CARDFLIPTIME radius:1 deltaRadius:0 angleZ:275 deltaAngleZ:(85 /*+ (g_->mirrored ? -30 : 30)*/) angleX:0 deltaAngleX:0],
								[CCCallFuncN actionWithTarget:self selector:@selector(dangerCountZero:)],
							  nil]];
}

- (void) dangerCountZero:(id)sender {
	if(deathnum_ == 0) dangercountzero_ = true;
}

- (void) die {
	// game over
	if(!GODMODE && dangercountzero_ && !won_ && !lost_) {
		lost_ = true;
		if(opponent_) {
			CCLabel* lose = [CCLabel labelWithString:@"LOSE"
									   fontName:@"Franchise"
									   fontSize:130];
			CGPoint start = WINLABELPOS;
			start.y += 400;
			lose.position = start;
			lose.color = ccc3(255,255,255);
			winlabel_ = lose;
			[self addChild:lose z:WINLOSE_Z];
			[lose runAction:[CCSequence actions:
				[CCMoveTo actionWithDuration:0.5 position:WINLABELPOS],
				[CCJumpTo actionWithDuration:0.3 position:WINLABELPOS height:50 jumps:1],
				nil]];
		}

		[[CCTouchDispatcher sharedDispatcher] removeDelegate:self];

		[self unschedule:@selector(gameLoop:)];
		[opponent_ winAnimation];
		if(scene.layout_ == SINGLEPLAYER || (scene.layout_ == SINGLEVSAI && !g_->is_ai)) {
			[scene playSound:@"Assets/Sounds/trash.caf" volume:1.0];
		}
		if(MUSIC && scene.layout_ == SINGLEPLAYER) {
			[scene pauseMusic];
		}

		[blocksdisplay_ cleanup];
		int fgopacity = (scene.layout_ == SINGLEPLAYER) ? 0xD0 : 0xA0;
		[foreground_ runAction:[CCFadeTo actionWithDuration:0.3 opacity:fgopacity]];

		[background_ stopAllActions];
		background_.visible = true;
		[background_ setColor:ccc3(0xFF, 0xFF, 0xFF)];
		[background_ setOpacity:0xFF];
		[background_ runAction:[CCTintTo actionWithDuration:0.3 red:0 green:0 blue:0]];

		[dangerglow_ stopAllActions];
		[dangerglow_ runAction:[CCFadeTo actionWithDuration:(DEATHCOUNTTIME/3) opacity:0]];

		[self unschedule:@selector(alarmSoundCallback:)];
		[scene stopSound:alarmsoundid_];

		[scene changeState:ENDING];

		if(g_->is_ai) {
			g_->ailevel++;
		}
	}
}

- (void) terminateWin {
	if(!won_) return;
	[self unschedule:@selector(gameLoop:)];
	[[CCTouchDispatcher sharedDispatcher] removeDelegate:self];
	[foreground_ runAction:[CCFadeTo actionWithDuration:0.3 opacity:0xA0]];
	[dangerglow_ stopAllActions];
	[dangerglow_ runAction:[CCFadeTo actionWithDuration:(DEATHCOUNTTIME/3) opacity:0]];

	for(int i = 0; i < 3; i++) [winemitter_[i] stopSystem];
}

- (void) createStatsView {
	if(statsview_) return;

#if FAKECHAINSTATS
	for(int i = 0; i < 12; i++) {
		numchains_[i]++;
		numcombos_[i]++;
	}
#endif

	scene->playinginterstitial_ = true;
	[scene playMusic:@"Assets/Music/Cloud 2.m4a"];

	statsview_ = [CCNode node];
	statsview_.position = ccp(PX_LEFT, PX_BOTTOM);
	[statsview_ setContentSize:CGSizeMake(PX_XSIZE, PX_YSIZE)];
	[self addChild:statsview_ z:STATS_Z];

	int dashx = 420 - 314;
	int labelx = dashx + 20;
	int ylinediff = 442-382;
	int xlinediff = 544-405;
	int xpos = 0;
	int ypos = PX_TOP - 100;

	int leftx = 352 - 312;
	int margin = leftx;
	int rightx = 726 - 312;

	NSString* str;
	CCLabel* label;

	str = [NSString stringWithFormat:@"Game Over"];
	label = [CCLabel labelWithString:str fontName:@"Franchise" fontSize:80];
	label.position = ccp(PX_XSIZE / 2, ypos);
	label.anchorPoint = ccp(0.5, 0);
	label.color = htmlColor("CCCCCC");
	[statsview_ addChild:label];
	ypos -= (225 - 158);

	int depth = MAX(0, -highestblocklevel_);
	str = [NSString stringWithFormat:@"Lines reached:"];
	label = [CCLabel labelWithString:str fontName:@"BPreplay" fontSize:38];
	label.color = htmlColor("CCCCCC");
	label.anchorPoint = ccp(0, 0);
	label.position = ccp(leftx, ypos);
	[statsview_ addChild:label];
	str = [NSString stringWithFormat:@"%d", depth];
	label = [CCLabel labelWithString:str fontName:@"BPreplayBold" fontSize:38];
	label.anchorPoint = ccp(1, 0);
	label.position = ccp(rightx, ypos);
	[statsview_ addChild:label];
	ypos -= (268 - 225);

	str = [NSString stringWithFormat:@"Beams cleared:"];
	label = [CCLabel labelWithString:str fontName:@"BPreplay" fontSize:38];
	label.color = htmlColor("CCCCCC");
	label.position = ccp(leftx, ypos);
	label.anchorPoint = ccp(0, 0);
	[statsview_ addChild:label];
	str = [NSString stringWithFormat:@"%d", blockscleared_];
	label = [CCLabel labelWithString:str fontName:@"BPreplayBold" fontSize:38];
	label.anchorPoint = ccp(1, 0);
	label.position = ccp(rightx, ypos);
	[statsview_ addChild:label];
	ypos -= (268 - 225);

	float percentspecial = 0.0f;
    if(blockscleared_ > 0) {
		percentspecial = (blockschained_ * 100.0) / blockscleared_;
	}
	str = [NSString stringWithFormat:@"Chain ratio:"];
	label = [CCLabel labelWithString:str fontName:@"BPreplay" fontSize:38];
	label.position = ccp(leftx, ypos);
	label.anchorPoint = ccp(0, 0);
	label.color = htmlColor("CCCCCC");
	[statsview_ addChild:label];
	str = [NSString stringWithFormat:@"%2.1f%%", percentspecial];
	label = [CCLabel labelWithString:str fontName:@"BPreplayBold" fontSize:38];
	label.anchorPoint = ccp(1, 0);
	label.position = ccp(rightx, ypos);
	[statsview_ addChild:label];

	ypos -= (382 - 306);
	int baseypos = ypos;

	int endypos = 1024 - 760;

	for(int i = MAX_CHAIN - 1; i >= 2; i--) {
		if(numchains_[i] <= 0) continue;

		// dash
		str = [NSString stringWithFormat:@"-"];
		label = [CCLabel labelWithString:str fontName:@"BPreplay" fontSize:25];
		label.position = ccp(dashx + xpos, ypos);
		label.anchorPoint = ccp(0, 0.5);
		[statsview_ addChild:label];

		// counter
		str = [NSString stringWithFormat:@"%d", numchains_[i], i];
		label = [CCLabel labelWithString:str fontName:@"BPreplayBold" fontSize:30];
		label.position = ccp(labelx + xpos, ypos);
		label.anchorPoint = ccp(0, 0.5);
		[statsview_ addChild:label];

		// xN Star
		str = [NSString stringWithFormat:@"x%d", i];
		label = [CCLabel labelWithString:str fontName:@"BPreplay" fontSize:30];
		label.color = ccc3(0,0,0);

		CCSprite* star = [CCSprite spriteWithFile:@"Assets/Graphics/chain-star-ds.png"];
		star.position = ccp(margin + xpos, ypos);
		star.anchorPoint = ccp(0, 0.5);
		CGSize starsize = [star contentSize];
		label.position = ccp(starsize.width/2, starsize.height/2 - 3);
		CCSprite* innerstar = [CCSprite spriteWithFile:@"Assets/Graphics/chain-star.png"];
		[innerstar setColor:chainColor(i - 2)];
		innerstar.position = ccp(starsize.width / 2, starsize.height / 2);
		[star addChild:innerstar];
		star.scale = 0.5;
		[star addChild:label z:2];
		[statsview_ addChild:star];

		ypos -= ylinediff;
		if(ypos < endypos) {
			ypos = baseypos; xpos += xlinediff;
	   	}
	}
	for(int i = MAX_CHAIN - 1; i >= 3; i--) {
		if(numcombos_[i] == 0) continue;

		// dash
		str = [NSString stringWithFormat:@"-"];
		label = [CCLabel labelWithString:str fontName:@"BPreplay" fontSize:25];
		label.position = ccp(dashx + xpos, ypos);
		label.anchorPoint = ccp(0, 0.5);
		[statsview_ addChild:label];

		// counter
		str = [NSString stringWithFormat:@"%d", numcombos_[i]];
		label = [CCLabel labelWithString:str fontName:@"BPreplayBold" fontSize:30];
		label.position = ccp(labelx + xpos, ypos);
		label.anchorPoint = ccp(0, 0.5);
		[statsview_ addChild:label];

		// +N circle
		str = [NSString stringWithFormat:@"+%d", i];
		label = [CCLabel labelWithString:str fontName:@"BPreplay" fontSize:15];

		CCSprite* circle = [CCSprite spriteWithFile:@"Assets/Graphics/combo.png"];
		label.position = ccp([circle contentSize].width/2, [circle contentSize].height/2);
		circle.position = ccp(margin + xpos, ypos);
		circle.anchorPoint = ccp(0, 0.5);
		[circle setOpacity:0x40];
		label.color = chainColor(i - 2);

		[circle addChild:label z:2];
		[statsview_ addChild:circle];

		ypos -= ylinediff;
		if(ypos < endypos) {
			ypos = baseypos; xpos += xlinediff;
		}
	}
	
	restartbutton_ = [CCSprite spriteWithFile:@"Assets/Graphics/1p-btn-play-again.png"];
	restartbutton_.position = ccp(leftx, 1024-884);
	restartbutton_.anchorPoint = ccp(0, 0);
	[statsview_ addChild:restartbutton_];

	endmenubutton_ = [CCSprite spriteWithFile:@"Assets/Graphics/1p-btn-return-to-menu.png"];
	endmenubutton_.position = ccp(leftx, 1024-976);
	endmenubutton_.anchorPoint = ccp(0, 0);
	[statsview_ addChild:endmenubutton_];
}

- (void) winAnimation {
	CCLabel* win = [CCLabel labelWithString:@"WIN!"
							   fontName:@"Franchise"
							   fontSize:150];
	win.position = g_->winstartpos;
	win.color = ccc3(255,255,255);
	winlabel_ = win;
	[self addChild:win z:WINLOSE_Z];
	[win runAction:[CCSequence actions:
		[CCJumpTo actionWithDuration:0.5 position:WINLABELPOS height:150 jumps:1],
		[CCEaseInOut actionWithAction:[CCRotateBy actionWithDuration:0.25 angle:15] rate:3.0f],
		// Can't have a RepeatForever in a Sequence for some reason...
	    [CCCallFuncN actionWithTarget:self selector:@selector(winAnimation2:)],
		nil]];

	CGPoint emitterpos = WINLABELPOS;
	emitterpos.y += 400;
	CCParticleWinRain* emitter;
   	emitter = [CCParticleWinRain node];
	emitter.texture = [[CCTextureCache sharedTextureCache] addImage: @"Assets/Graphics/hat.png"];
	emitter.position = emitterpos;
	emitter.posVar = ccp(PX_XSIZE / 2, 0);
	emitter.emissionRate = 3;
	winemitter_[0] = emitter;
	[self addChild:emitter z:CONFETTI_Z];
   	emitter = [CCParticleWinRain node];
	emitter.texture = [[CCTextureCache sharedTextureCache] addImage: @"Assets/Graphics/chain-star.png"];
	emitter.position = emitterpos;
	emitter.posVar = ccp(PX_XSIZE / 2, 0);
	ccColor3B c = htmlColor("1297D4");
	ccColor4F startColor = {c.r/255.0, c.g/255.0, c.b/255.0, 1.0f};
	emitter.startColor = startColor;
	emitter.endColor = emitter.startColor;
	emitter.emissionRate = 10;
	winemitter_[1] = emitter;
	[self addChild:emitter z:CONFETTI_Z];
   	emitter = [CCParticleWinRain node];
	emitter.texture = [[CCTextureCache sharedTextureCache] addImage: @"Assets/Graphics/hearts/man0/heart-0005.png"];
	emitter.position = emitterpos;
	emitter.posVar = ccp(PX_XSIZE / 2, 0);
	emitter.emissionRate = 10;
	winemitter_[2] = emitter;
	[self addChild:emitter z:CONFETTI_Z];

//	if(dangersign_) { dangersign_.visible = false; }
	won_ = true;

	[dangerglow_ stopAllActions];
	[dangerglow_ runAction:[CCFadeTo actionWithDuration:(DEATHCOUNTTIME/3) opacity:0]];
	[self unschedule:@selector(alarmSoundCallback:)];
	[scene stopSound:alarmsoundid_];

	if(MUSIC && scene.layout_ == SINGLEPLAYER) {
//		[scene playMusic:@"Assets/Music/EBDN.m4a"];
		scene->playinginterstitial_ = true;
	}
	if(!g_->is_ai) {
		[scene playSound:@"Assets/Sounds/fanfare-long.caf" volume:1.0];
	}

	g_->wins++;
}

- (void) winAnimation2:(id)sender {
	CCSprite* win = sender;
	// make win animation cheerfully rotate repeatedly
	[win runAction:[CCRepeatForever actionWithAction:[CCSequence actions:
		[CCEaseInOut actionWithAction:[CCRotateBy actionWithDuration:0.5 angle:-30] rate:3.0f],
		[CCEaseInOut actionWithAction:[CCRotateBy actionWithDuration:0.5 angle:30] rate:3.0f],
			nil]]];

}


- (ccTime) aiTime {
	return 5.0f / (g_->ailevel * 0.4f + 1.5f);
}

enum AIActionType {
	NOTHING,
	RANDOM,      // place block anywhere
	RANDOMLANDED, // place block on ground
	RANDOMMATCH, // match a random block
	COMBOMATCH,  // match on the biggest combo available
	TRASHMATCH,  // match intended to clear trash
	CHAINMATCH,  // match intended to trigger a chain
	PREPCHAIN,   // place a block in a comboable position
	PREPTRASH,   // place a block near trash
};
typedef enum AIActionType AIActionType;

struct AIBlockPlacement {
	int x, y;
	BlockShape shape;
	bool landed;
	int combo;
};
typedef struct AIBlockPlacement AIBlockPlacement;

- (Block*) aiPlay:(bool)tutorialmode {
	AIBlockPlacement possible[GRID_XMAX * GRID_YMAXNOTRASH * 4];
	int c = 0;

	BlockShape firstshape = LEFT, lastshape = DOWN;
	if(currentblock_.shape == LEFTRIGHT) {
		firstshape = LEFTRIGHT;
		lastshape = UPDOWN;
	}

	// Fill this "variable-size" array (number of elements stored in 'c') with all legal block placements
	int index[3];
	int x, y;
	BlockShape s;
	for(y = 0; y < GRID_YSIZE; y++) {
		for(x = 0; x < GRID_XSIZE; x++) {
			for(s = firstshape; s <= lastshape; s = (BlockShape) (s << 1)) {
				if(s & LEFT || s & RIGHT) {
					index[0] = y * GRID_XMAX + x;
					index[1] = y * GRID_XMAX + x+1;
					index[2] = y * GRID_XMAX + x+2;
				} else {
					index[0] = y * GRID_XMAX + x;
					index[1] = (y+1) * GRID_XMAX + x;
					index[2] = (y+2) * GRID_XMAX + x;
				}
				if(!covergrid_[index[0]] && !covergrid_[index[1]] && !covergrid_[index[2]]) {
					possible[c].x = x;
					possible[c].y = y;
					possible[c].shape = s;
					possible[c].landed = false;
					possible[c].combo = 0;
					c++;
				}
			}
		}
	}

	int numlanded = 0;
	// Check if will be placed immediately on board or has to fall
	for(int i = 0; i < c; i++) {
		x = possible[i].x;
		y = possible[i].y;
		s = possible[i].shape;

		if(y == 0 || blockgrid_[(y-1)*GRID_XMAX+x]) {
			possible[i].landed = true;
			numlanded++;
			continue;
		}
		if(s & LEFT || s & RIGHT) {
			if(blockgrid_[(y-1)*GRID_XMAX+(x+1)] || blockgrid_[(y-1)*GRID_XMAX+(x+2)]) {
				possible[i].landed = true;
				numlanded++;
			}
		}
	}

	BlockColor coltocheck = [common_ getBlockColorToPlace:blocksplaced_];
	int highcombo = 0;
	// Check for heart matches around landed possibilities.
	for(int i = 0; i < c; i++) {
		if(!possible[i].landed) continue;
		int heartx[2], hearty[2];
		heartx[0] = heartx[1] = hearty[0] = hearty[1] = -1000;
		int checkoffset = 0;

		x = possible[i].x;
		y = possible[i].y;
		s = possible[i].shape;
		if(s & LEFT || s & DOWN) {
			heartx[checkoffset] = x;
			hearty[checkoffset] = y;
			checkoffset++;
		}
		if(s & RIGHT) {
			heartx[checkoffset] = x + 2;
			hearty[checkoffset] = y;
			checkoffset++;
		}
		if(s & UP) {
			heartx[checkoffset] = x;
			hearty[checkoffset] = y + 2;
			checkoffset++;
		}

		for(int k = 0; k < checkoffset; k++) {
			y = hearty[k];
			for(x = heartx[k] - 1; x <= heartx[k] + 1; x += 2) {
				if(x < 0 || y < 0) continue;
				if(heartgrid_[y * GRID_XMAX+ x] == coltocheck) {
					possible[i].combo += 1;
				}
			}
			x = heartx[k];
			for(y = hearty[k] - 1; y <= hearty[k] + 1; y += 2) {
				if(x < 0 || y < 0) continue;
				if(heartgrid_[y * GRID_XMAX+ x] == coltocheck) {
					possible[i].combo += 1;
				}
			}
		}
		if(possible[i].combo > highcombo) {
			highcombo = possible[i].combo;
		}
	}


	// Decide what kind of action to take.
	int matchprob = 50 + (g_->ailevel * 3);
	if(tutorialmode) matchprob = 100;
	AIActionType action;
	if(myrandom(100) < matchprob) {
		action = RANDOMMATCH;
	} else {
		action = RANDOM;
	}

	if((action == RANDOMMATCH || action == COMBOMATCH) && highcombo == 0) {
		action = RANDOM;
	}

	if(c == 0) {
		action = NOTHING;
	}

	int choice = 0;
	if(action == NOTHING) {
		return nil;
	}

	if(action == RANDOM) {
		choice = myrandom(c);
	}

	if(action == RANDOMMATCH || action == COMBOMATCH || action == RANDOMLANDED) {
		if((action == RANDOMMATCH || action == COMBOMATCH) && highcombo == 0) assert(false);
		int c2 = 0;
		// Collapse into smaller set of choices containing only matches
		for(int i = 0; i < c; i++) {
			bool valid = false;
			if(action == RANDOMLANDED && possible[i].landed) valid = true;
			if(action == COMBOMATCH && possible[i].combo >= highcombo) valid = true;
			if(action == RANDOMMATCH && possible[i].combo >= 1) valid = true;
			if(valid) {
				possible[c2] = possible[i];
				c2++;
			}
		}
		if(c2 == 0) return nil;
		choice = myrandom(c2);
		if(tutorialmode) choice = c2 - 1;
	}
	CGPoint spawnpointgrid = ccp(possible[choice].x, possible[choice].y);
	BlockShape currentshape = possible[choice].shape;

	if(!tutorialmode) {
		spawnpointgrid_ = spawnpointgrid;
		currentshape_ = currentshape;
		[self throwBlock];
		return nil;
	} else {
		return [Block blockInGrid:g_ pos:spawnpointgrid
			color:[common_ getBlockColorToPlace:blocksplaced_]
			shape:currentshape];
	}
}

- (void) placeTutorialBlock {
	if(tutorialblock_) {
		bool badpos = false;
		for(Block* b2 in blocks_) {
			if(b2.ismelting) continue;
			if(CGRectIntersectsRect([tutorialblock_ gridRect], [b2 gridRect])) {
				badpos = true;
				break;
			}
		}
		if(badpos) {
			[self removeTutorialBlock];
		} else {
			return;
		}
	}
	tutorialblock_ = [self aiPlay:true];
	if(tutorialblock_ == nil) return;
	[tutorialblock_ updateDisplay];
	[tutorialblock_ makeTutorialBlock:self];
	[blocksdisplay_	addChild:tutorialblock_ z:CURSOR_Z];
}

- (void) removeTutorialBlock {
	[blocksdisplay_	removeChild:tutorialblock_ cleanup:true];
	tutorialblock_ = nil;
}

-(void) pause {
	if(ingamestate_ == PLAY) {
		[self unschedule: @selector(gameLoop:)];
	}
}

-(void) unpause {
	if(ingamestate_ == PLAY && scene.state_ != ENDED_SOLO) {
		[self schedule: @selector(gameLoop:) interval:FRAMETIME];
	}
}


- (void) destroyRobotAnimation {
	[scene playMusic:@"Assets/Music/Cloud 2.m4a"];
	[scene pauseMusic];
	rumblesound_ = [scene playSound:@"Assets/Sounds/rumble.caf" volume:1.0];

	// Fade out just the blocks on our side.
	[self hideSideElements];
	crouched_ = false;
	[self crouch];
	[opponent_ hideSideElements];

	[self schedule: @selector(ledCallback:) interval:0.03];
	roboshakemult_ = 1.0;
	[self schedule: @selector(shakeCallback:) interval:0.26];
	[self schedule: @selector(bsodCallback:) interval:7.0];
}

- (void) ledCallback:(ccTime)dt {
	lednumber_++;
	if(lednumber_ >= 100) lednumber_ = 0;
	[self setRobotLED:lednumber_];
}

- (void) shakeCallback:(ccTime)dt {
	roboshakemult_ *= 1.1;
	double mult = roboshakemult_;

	[mascot_ runAction:[CCSequence actions:
											   [CCMoveBy actionWithDuration:0.05 position:ccp(10*mult,10*mult)],
											   [CCMoveBy actionWithDuration:0.05 position:ccp(-20*mult,-20*mult)],
											   [CCMoveBy actionWithDuration:0.05 position:ccp(15*mult,15*mult)],
											   [CCMoveBy actionWithDuration:0.05 position:ccp(-10*mult, -10*mult)],
											   [CCMoveTo actionWithDuration:0.05 position:MASCOTPOS],
																	   nil]];
}

- (void) bsodCallback:(ccTime)dt {
	[self unschedule:@selector(ledCallback:)];
	[self unschedule:@selector(shakeCallback:)];
	[self unschedule:@selector(bsodCallback:)];
	[mascot_ stopAllActions];
	[scene stopSound:rumblesound_];
	bsodsound_ = [scene playSound:@"Assets/Sounds/whine.caf" volume:1.0];
	mascot_.position = MASCOTPOS;

	[mascot_ setTexture:[[CCTextureCache sharedTextureCache] addImage:[NSString stringWithFormat:@"Assets/Graphics/char3-v2/char3-explode.png"]]];

	robotLEDbox_.visible = false;

	[self runAction:[CCSequence actions:
									   [CCDelayTime actionWithDuration:3.0],
									   [CCCallFuncN actionWithTarget:self selector:@selector(explodeRobot:)],
															   nil]];
}

- (void) explodeRobot:(id)sender {
	[scene stopSound:bsodsound_];

	int xsize = 225, ysize = 400;
	int midx = 143, midy = 275;
	int xdist = xsize/4, ydist = ysize/4;
	mascot_.visible = false;
	for(int i = 0; i < 4; i++) {
		robotfragments_[i] = [CCSprite spriteWithTexture:emptytex];
		robotfragments_[i].scale = MASCOTSCALE;
		CGPoint pos = MASCOTPOS;
		CGPoint endpos = MASCOTPOS;
		if(g_->mirrored) { robotfragments_[i].flipX = true; }
		[robotfragments_[i] setTexture:mascottex_[1]];
		switch(i) {
			case 0:
				[robotfragments_[i] setTextureRect: CGRectMake(0, 0, midx, midy)];
				pos.x -= xdist;
				pos.y += ydist;
				endpos.x -= 1000;
				endpos.y += 1000;
				break;
			case 1:
				[robotfragments_[i] setTextureRect: CGRectMake(midx, 0, xsize, midy)];
				pos.x += xdist;
				pos.y += ydist;
				endpos.x += 1000;
				endpos.y += 1000;
				break;
			case 2:
				[robotfragments_[i] setTextureRect: CGRectMake(0, midy, midx, ysize)];
				pos.x -= xdist;
				pos.y -= ydist;
				endpos.x -= 1000;
				endpos.y += 200;
				break;
			case 3:
				[robotfragments_[i] setTextureRect: CGRectMake(midx, midy, xsize, ysize)];
				pos.x += xdist;
				pos.y -= ydist;
				endpos.x += 1000;
				endpos.y += 200;
				break;
		}
		robotfragments_[i].position = pos;
		[self addChild:robotfragments_[i] z:MASCOT_Z];
		[robotfragments_[i] runAction:[CCSequence actions:
						   [CCMoveTo actionWithDuration:4 position:endpos],
						   nil]];
		[robotfragments_[i] runAction:[CCSequence actions:
						   [CCRotateBy actionWithDuration:4 angle:360*8],
						   nil]];
	}

	[scene playSound:@"Assets/Sounds/explode.caf" volume:1.0];

	[scene resumeMusic];

	CCSprite* youwin = [CCSprite spriteWithFile:@"Assets/Graphics/win-screen-main.png"];
	bool wasunlocked = [[NSUserDefaults standardUserDefaults] boolForKey:D_ROBOTUNLOCKED];
	[[NSUserDefaults standardUserDefaults] setBool:true forKey:D_ROBOTUNLOCKED];
	CCSprite* robotunlocked = [CCSprite spriteWithFile:@"Assets/Graphics/win-unlock-steelbot.png"];
	robotunlocked.visible = !wasunlocked;
	int maxspeed = [[NSUserDefaults standardUserDefaults] integerForKey:D_ROBOTMAXSPEED];
	maxspeed += 1;
	[[NSUserDefaults standardUserDefaults] setInteger:maxspeed forKey:D_ROBOTMAXSPEED];
	CCSprite* maxspeedlabel = [CCSprite spriteWithFile:@"Assets/Graphics/win-unlock-speed.png"];
	youwin.position = ccp(-300, 0);
	robotunlocked.position = ccp(-300, 0);
	maxspeedlabel.position = ccp(-300, 0);
	youwin.anchorPoint = ccp(0.5, 0);
	maxspeedlabel.anchorPoint = ccp(0.5, 0);
	robotunlocked.anchorPoint = ccp(0.5, 0);
	[youwin runAction:[CCSequence actions:
       	[CCDelayTime actionWithDuration:3.0], 
		[CCEaseOut actionWithAction:[CCMoveTo actionWithDuration:0.8 position:ccp(512, 0)] rate:3.0f],
        [CCCallFuncN actionWithTarget:self selector:@selector(bangSound:)],
		nil]];
	[maxspeedlabel runAction:[CCSequence actions:
       	[CCDelayTime actionWithDuration:4.0], 
		[CCEaseOut actionWithAction:[CCMoveTo actionWithDuration:0.8 position:ccp(512, 0)] rate:3.0f],
        [CCCallFuncN actionWithTarget:self selector:@selector(clingSound:)],
		[CCDelayTime actionWithDuration:1.5], 
        [CCCallFuncN actionWithTarget:self selector:@selector(endExplosion:)],
		nil]];
	if(robotunlocked.visible) {
		[robotunlocked runAction:[CCSequence actions:
			[CCDelayTime actionWithDuration:4.8], 
			[CCEaseOut actionWithAction:[CCMoveTo actionWithDuration:0.8 position:ccp(512, 0)] rate:3.0f],
			[CCCallFuncN actionWithTarget:self selector:@selector(clingSound:)],
			nil]];
	}

	maxnums_[0] = [CCSprite spriteWithTexture:maxLEDtex_[MIN(maxspeed / 10, 9)]];
	maxnums_[1] = [CCSprite spriteWithTexture:maxLEDtex_[maxspeed % 10]];
	maxnums_[0].position = ccp(256, 768 - 546);
	maxnums_[1].position = ccp(256 + 40, 768 - 546);
	maxnums_[0].anchorPoint = ccp(0.5, 0);
	maxnums_[1].anchorPoint = ccp(0.5, 0);
	[maxspeedlabel addChild:maxnums_[0]];
	[maxspeedlabel addChild:maxnums_[1]];

	[self addChild:youwin z:WINLOSE_Z];
	[self addChild:robotunlocked z:WINLOSE_Z];
	[self addChild:maxspeedlabel z:WINLOSE_Z];
}

-(void) bangSound:(id)sender {
	[scene playSound:@"Assets/Sounds/trash.caf" volume:1.0];
}

-(void) clingSound:(id)sender {
	[scene playSound:@"Assets/Sounds/fanfare-long.caf" volume:1.0];
}

-(void) endExplosion:(id)sender {
	[scene changeState:ENDED_VSAI_COMPLETE];
}

- (void) hideSideElements {
	[mascot_ runAction:[CCFadeOut actionWithDuration:0.1]];
	[fingers_ runAction:[CCFadeOut actionWithDuration:0.1]];
	[shadow_ runAction:[CCFadeOut actionWithDuration:0.1]];
	if(robotlogo_) [robotlogo_ runAction:[CCFadeOut actionWithDuration:0.1]];
	[currentblock_ runAction:[CCFadeOut actionWithDuration:0.1]];
	[nextblock_    runAction:[CCFadeOut actionWithDuration:0.1]];
	[nextblock_->heart1    runAction:[CCFadeOut actionWithDuration:0.1]];
	[nextblock_->heart2    runAction:[CCFadeOut actionWithDuration:0.1]];
	[currentblock_->heart1    runAction:[CCFadeOut actionWithDuration:0.1]];
	[currentblock_->heart2    runAction:[CCFadeOut actionWithDuration:0.1]];
	[dangersign_ runAction:[CCFadeOut actionWithDuration:0.1]];
	[self speechBubble:nil];
}

- (void) speechBubble:(NSString*)chatkey {
	if(speech_) {
		[fingers_ removeChild:speech_ cleanup:true];
	}
	if(chatkey == nil) {
		speechbubble_.visible = false;
		return;
	}

	NSString* chatvalue = [scene->chatdict_ valueForKey:chatkey];
	int fontsize = 19;
	if([chatvalue length] > 50) fontsize = 16;
	speech_ = [CCLabel labelWithString:chatvalue
                  		   dimensions:CGSizeMake(235*0.7, 130*0.7)
                           alignment:UITextAlignmentCenter
						   fontName:@"BPreplay"
						   fontSize:fontsize];
	speech_.color = ccc3(0,0,0);
	speech_.anchorPoint = ccp(0.5,0.5);
	CGPoint pos = speechbubble_.position;
	speech_.position = pos;
	[fingers_ addChild:speech_];
	speechbubble_.visible = true;
}

- (Mascot) mascot {
	return g_->mascot;
}

- (void) showWins {
	CGPoint pos = ccp(PX_LEFT + PX_XSIZE/2-100, PX_YSIZE * 0.2);
	CGPoint startpos = ccp(pos.x, pos.y - 300);
	winscount_ = [CCLabel labelWithString:@"Wins:"  
				   fontName:@"BPreplay"
				   fontSize:60];
	winscount_.anchorPoint = ccp(0,0);
	winscount_.position = startpos;
	winscount_.color = ccc3(0xAA, 0xAA, 0xAA);
	[self addChild:winscount_ z:WINLOSE_Z];
	[winscount_ runAction:[CCEaseIn actionWithAction:[CCMoveTo actionWithDuration:0.5 position:pos] rate:3.0f]];

	CCLabel* count = [CCLabel labelWithString:[NSString stringWithFormat:@"%d", g_->wins]
				   fontName:@"BPreplayBold"
				   fontSize:60];
	count.position = ccp([winscount_ contentSize].width + 20, 0);
	count.anchorPoint = ccp(0, 0);
	[winscount_ addChild:count];
}

@end
